朝日ネット 技術者ブログ

朝日ネットのエンジニアによるリレーブログ。今、自分が一番気になるテーマで書きます。

Pythonのパッケージングのベストプラクティスについて考える2018

はじめに

開発部の tasaki です。Python 3.7 のリリースが今月末に行われるということで、あらためて 2018 年現在の Python のパッケージ構成におけるベストプラクティスについて検討してみたいと思います。

対象読者

この記事は、

  • 書き捨ての Python スクリプトなら書けるが、ちゃんとしたパッケージの作り方がよく分からない
  • 公式リファレンスのモジュールの章を読んだが、結局具体的にどういう構成にすればよいのか分からない
  • setuptools.setup 関数の大量の引数のどれを使えばよいのか分からない

というような人を対象としています。

対象バージョン

処理系とツールチェーンのバージョンは、

  • Python 3.4 (2014/03/16 リリース)以降
  • pip 8.1.2 以降
  • setuptools 19.2 以降

を対象とします。 EPEL の python34, python34-pip, python34-setuptools がこのバージョンであり、これらが使われている環境はかなり多いと考えられます 1

結論 (TL;DR)

Flask のパッケージ構成がわかりやすくソースコードの規模感も丁度よいため、できるだけこの構成を踏襲するとよいでしょう。

今回使ったソースコードは GitHub に置いてあります。

テスト可能な最小構成を作る

Flask を参考に最小構成の、ただし pytest と tox によるテストができるパッケージを作ってみましょう。 パッケージ名は python_boilerplate とし、最低限以下のようなことができるものとします。

  • pip install . でパッケージをインストールできる
    • インストール後、他パッケージから import python_boilerplate できる
    • インストール後、python_boilerplate コマンドでアプリケーションを実行できる
  • pip install -e '.[dev]' で開発者向けの依存パッケージも含めてインストールできる
    • pytest コマンドでテストが走る
    • tox コマンドで複数バージョンの Python 処理系でテストが走り、カバレッジレポートが出力される
  • python setup.py bdist_wheel で wheel パッケージ(ビルド済み配布物)を作れる
    • この wheel パッケージは pip install python_boilerplate-x.y.z-py3-none-any.whl でインストールできる

ファイル構成

ディレクトリとファイルの配置は以下のようになります。

.
├── setup.py
├── MANIFEST.in
├── python_boilerplate
│   ├── __init__.py
│   ├── __main__.py
│   ├── cli.py
│   └── core.py
├── tests
│   └── test_core.py
├── setup.cfg
├── tox.ini
├── README
├── docs
│   └── ...
└── examples
    └── ...

ファイルの内容

setup.py

setuptools.setup 関数を呼び出すメインの設定スクリプトです。setup.py は python setup.py <サブコマンド> の形で使われたり pip 経由で実行されたりします。

import ast
import re
import os

from setuptools import setup

PACKAGE_NAME = 'python_boilerplate'

with open(os.path.join(PACKAGE_NAME, '__init__.py')) as f:
    match = re.search(r'__version__\s+=\s+(.*)', f.read())
version = str(ast.literal_eval(match.group(1)))

setup(
    # metadata
    name=PACKAGE_NAME,
    version=version,

    # options
    packages=[PACKAGE_NAME],
    include_package_data=True,
    zip_safe=False,
    python_requires='>=3.4',
    install_requires=[],
    extras_require={
        'dev': [
            'pytest>=3',
            'coverage',
            'tox',
        ],
    },
    entry_points='''
        [console_scripts]
        {app}={pkg}.cli:main
    '''.format(app=PACKAGE_NAME.replace('_', '-'), pkg=PACKAGE_NAME),
)

setup 関数には他にも沢山の引数がありますが最低限これだけあればよさそうです 2PyPI にパッケージを公開する場合はより多くのメタ情報(Metadata)を加えた方がよいでしょう。

バージョンについては「正規表現でバージョン情報を __init__.py から抜き出し、それを文字列リテラルとして評価する」ということをやっています。 その他、exec を使う、VERSION ファイルを使うなどの手法もあり、以下のページが参考になります。

その他の引数の意味については以下の通りです。

  • include_package_data
    • True の場合、MANIFEST.in 記載のファイルまたは package_data に指定したファイルをビルド済み配布物 (bdist) に含める(「補足事項: wheel パッケージにデータファイルを含める」にて後述) (Including Data Files)
  • zip_safe
    • True の場合 zip されたパッケージを展開せずにスクリプトを実行することを許可する (Setting the zip_safe flag)
      • python setup.py install 時に site-packages 下に zip 形式の egg パッケージがそのまま配置される
    • pip install する場合には関係ないが、念のため False にしておく
  • install_requires
  • extras_require
  • entry_points
    • pip install 時に指定したメソッドを呼び出すコマンドラインアプリケーションがグローバル 3 にインストールされる (Dynamic Discovery of Services and Plugins)
    • この例では python_boilerplate.cli パッケージの main 関数を呼ぶ CLI アプリケーションが python-boilerplate 実行可能ファイルとしてインストールされる

より詳細な情報については setuptools の公式ページを参照してください。

MANIFEST.in

Python パッケージの配布形式としてはソースコード配布物(source distribution, sdist)とビルド済み配布物 (built distribution, bdist) がありますが、 前者のソースコード配布物に含めるべきファイルを MANIFEST.in で指定します 4

include tox.ini

graft tests
graft examples
graft docs

global-exclude *.py[co]

prune docs/_build
prune docs/_themes

コマンドの意味は以下の通りです(Python モジュールの配布 (レガシーバージョン)コマンドリファレンス より抜粋)。

  • include: 指定したパターンにマッチする全てのファイルを含める
  • graft: 指定したディレクトリ配下の全てのファイルを含める
  • prune: 指定したディレクトリ配下の全てのファイルを除外する
  • global-exclude: ソースツリー内にある、指定したパターンにマッチする全てのファイルを除外する
python_boilerplate/__init__.py

setup.py から参照するため __version__ = 文字列リテラル の形でバージョン情報を埋め込んでおきます。また、python_boilerplate パッケージのトップレベルに置きたい変数・関数・クラスを再エクスポートします。

__version__ = '1.0.0'

from .core import add

__all__ = ['add']
python_boilerplate/__main__.py

python -m python_boilerplate を実行するとこのコードが実行されます。 ここでは cli モジュールの main 関数に処理を丸投げします。

if __name__ == '__main__':
    from .cli import main
    main()
python_boilerplate/cli.py

プログラムをコマンドライン引数を受け取るアプリケーションとして実行するためのコードをここに書きます。setup.py の entry_points でこの main 関数が指定されています。

def main():
    import sys
    from .core import add
    if len(sys.argv) == 3:
        x, y = map(int, sys.argv[1:])
        print(add(x, y))
    else:
        print('please specify 2 arguments', file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()

なお、コマンドライン引数のパースには標準の argparse ライブラリやサードパーティ製の click を使うとよいでしょう。

python_boilerplate/core.py

今回はこのファイルにメインロジックを書いています。

def add(x, y):
    return x + y
tests/test_core.py

ここに pytest から実行されるテストコードを書きます。

from python_boilerplate import add

def test_add():
    assert add(1, 1) == 2
setup.cfg

setup.cfg は本来は distutils (setuptools の根底にあるライブラリ)の設定ファイルですが、サードパーティ製のライブラリもこの setup.cfg に書かれた設定を読む(読める)ことが多いです。 ここでは pytest と coverage の設定をします。

[tool:pytest]
minversion = 3.0
testpaths = tests

[coverage:run]
branch = True
source =
    python_boilerplate
    tests

[coverage:paths]
source =
    python_boilerplate
    .tox/*/lib/python*/site-packages/python_boilerplate
tox.ini

tox は様々な処理系・様々な依存ライブラリのバージョンの組み合わせでテストを実行してくれるテストランナーです。 ここでは tox.ini(設定ファイル)に Python 3.4 から 3.7 までの処理系を対象としてテストを実行し、カバレッジレポートを出力するよう指定しています。

[tox]
envlist =
    py{37,36,35,34}
    coverage-report
skip_missing_interpreters = true

[testenv]
passenv = LANG
deps =
    pytest>=3
    coverage
commands =
    coverage run -p -m pytest tests

[testenv:coverage-report]
deps = coverage
skip_install = true
commands =
    coverage combine
    coverage report
    coverage html

開発フロー

venv 環境の用意

Python 3.4 からは Python 仮想環境を作成する venv が標準パッケージとして用意されています(従来の virtualenv のような仕組みです)。 開発中はシステムやユーザの Python 環境 5 を汚さないように venv を使った方がよいでしょう。

python3 -m venv .venv
. .venv/bin/activate

カレントディレクトリのパッケージをインストール

setup.py の存在するディレクトリで以下のコマンドを打つとパッケージが環境にインストールされます。

pip install .
pip install --upgrade .  # アップグレードの場合(-U でもよい)

別のライブラリから python_boilerplate がライブラリとして import できるようになります。

import python_boilerplate
print(python_boilerplate.add(1, 1))  # => 2

また、setup 関数の entry_points で指定した関数を呼ぶスクリプトが bin/ 下にインストールされます。

$ type python-boilerplate
python-boilerplate is /path/to/.venv/bin/python-boilerplate
$ python-boilerplate 1 1
2

開発者向けのパッケージインストール

開発時には以下のコマンドでパッケージをインストールするとよいでしょう。

pip install -e '.[dev]'

-e (--editable) オプションを付けることで開発モード(編集可能モード)でインストールされます。 通常、カレントディレクトリ下のソースを編集しても再度 pip install するまで site-packages 下のファイルに変更は反映されません。 -e オプションを付けると site-packages 下に python-boilerplate.egg-link というファイル 6 が作成され、 カレントディレクトリのソースコードに対する変更が即座にパッケージへの変更として反映されるようになります。

また、インストールするパッケージ(ここでは .)の後に [dev] 7 を付けると setup 関数の extras_requiredev として指定したパッケージが依存パッケージとして同時にインストールされます。

pytest を使ったテスト

pytest コマンドを実行するとテストが実行されます。

pytest

setup.cfg で指定したディレクトリ以下の test_*.py または *_test.py がロードされ、test_ で始まる関数(および Test* クラスの test_* メソッド)が実行されます。

tox を使ったテスト

tox コマンドで複数の処理系・依存ライブラリのバージョンに対してテストを行えます。

tox

また、htmlcov/ 下にカバレッジレポートが生成されます。

ソースコード配布物の作成

以下のコマンドで dist/python_boilerplate-1.0.0.tar.gz が生成されます。ソースコード配布物には MANIFEST.in で指定したファイルが含まれます。

python setup.py sdist

ソースコード配布物を pip でインストールすることも可能です。

pip install --upgrade python_boilerplate-1.0.0.tar.gz

wheel 形式のビルド済み配布物の作成

wheel 形式 (*.whl) は PEP 427 で定められているビルド済み配布物のフォーマットであり、現在のデファクトスタンダードです。 これを作成するには最初に wheel パッケージをインストールしておく必要があります。

pip install wheel

以下のコマンドで dist/python_boilerplate-1.0.0-py3-none-any.whl が生成されます。

python setup.py bdist_wheel

wheel パッケージには tox.ini や tests/ などパッケージディレクトリ(ここでは python_boilerplate/ )の外にあるファイルは含まれていません。 デプロイ時はこのファイルをサーバに配布して pip でインストールするとよいでしょう。

pip install --upgrade python_boilerplate-1.0.0-py3-none-any.whl

補足事項

wheel パッケージにデータファイルを含める

テキストファイル・JSON ファイル・CSV ファイルなど、Python のソースファイル (*.py) ではないデータファイルを wheel パッケージに含めたい場合を考えます。 ここでは、python_boilerplate/data/foo.json をそのファイル名とします。

MANIFEST.in に何も指定がない場合、ソースファイル配布物 (sdist) にも wheel パッケージにも foo.json は含まれません。 MANIFEST.in に以下の記述を追加してみます。

include python_boilerplate/data/*.json

ここで、setup 関数の include_package_data 引数を False と指定した場合、sdist には foo.json は含まれますが wheel パッケージには foo.json は含まれません。 include_package_data=True とすることで MANIFEST.in で指定したパッケージ内のデータファイルを wheel パッケージに含めることができます。

詳細は setuputils のリファレンスの Including Data Files の章を参照してください。

API ドキュメント出力 (sphinx)

API ドキュメント(Javadoc 的なもの)を生成したいだけなら sphinx-quickstart コマンドより sphinx-apidoc コマンドを使った方が手軽です。sphinx パッケージをインストールした状態で以下を実行します。

if [ -e docs/conf.py ]; then
    sphinx-apidoc -f -o docs/ python_boilerplate/  # 2回目以降
else
    sphinx-apidoc -F -o docs/ python_boilerplate/  # 初回実行
fi
make -C docs/ html

.gitignore

.gitignore は github/gitignorePython.gitignore をそのまま使えばよさそうです。

curl -L https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore -o .gitignore

setup.cfg に setup 関数の引数を書く

setuptools 30.3.0 からは setup.cfg に setup 関数の引数のほとんどを書けるようになりました (Configuring setup() using setup.cfg files)。 setuptools のバージョンを気にしなくてよい場合はこちらの記法を使ってもよさそうです。

setup.cfg(抜粋)

バージョン情報については VERSION ファイルから取得する方法に変更しています。

[metadata]
name = python_boilerplate
version = file:python_boilerplate/VERSION

[options]
packages = python_boilerplate
include_package_data = True
zip_safe = False
python_requires = >=3.4
install_requires =

[options.extras_require]
dev =
    pytest>=3
    coverage
    tox

[options.entry_points]
console_scripts =
    python_boilerplate = python_boilerplate.cli:main
setup.py

setup.py は setup 関数を呼ぶだけになりました。

from setuptools import setup

setup()

pyproject.toml

PEP 518 で pyproject.toml というファイルでビルド時に必要な依存パッケージを設定できるよう定められており、pip 10 はこれに対応しています。 また、pyproject.toml は setuptools 代替のビルドシステム(flit, Poetry など)用の設定ファイルとしても使われつつあります (PEP 517)。

他のパッケージ構成例

参考

おわりに

今回は 2018 年現在の Python のパッケージ構成のベストプラクティスについて考えてみました。 「最小構成を作る」と言いつつとても長い記事になってしまいました……。

Python のパッケージングは歴史的な事情もあり非常に複雑です。 そして setuptools や pip のバージョンアップ、pyproject.toml の標準化、setuptools 代替のツールの出現など、今後エコシステムがより複雑化していく可能性もあります。 しかし新しいツールが現実的に使われ出すにはそれなりの時間がかかるはずで、まずは現在最も使われており安定している setuptools の使い方について習熟するのがよいと思います。 この記事がその手助けとなれば幸いです。

追記 (2018/11/19)

続編となる記事「2019年に向けてPythonのモダンな開発環境について考える」を書きましたのでよろしければそちらもご覧ください。

techblog.asahi-net.co.jp

更新履歴

  • 2018/06/15
    • 初版
  • 2018/11/19
    • 続編の記事に合わせてディレクトリ名・パッケージ名を変更
    • ikasat/python-boilerplate リポジトリへのリンクを追加
    • tox.ini に skip_missing_interpreters = true を追加

採用情報

朝日ネットでは新卒採用・キャリア採用を行っております。


  1. ちなみに Red Hat Enterprise Linux 8(および CentOS 8)からは Python 3 系が標準のリポジトリに入る(というより 2 系から置き換えられる)ようです。

  2. python setup.py sdist 時に警告を出したくない場合は url, author, author_email も追加してください。

  3. Linux では例えば /usr/bin や ~/.local/bin (--user 指定時)など(ディストリビューションによって異なります)。

  4. setup 関数で include_package_data を True とした場合、MANIFEST.in にビルド済み配布物に同梱するファイルを指定することもあります(「補足事項: wheel パッケージにデータファイルを含める」を参照)。

  5. Linux では例えば /usr/lib/python3.x/site-packages や ~/.local/lib/python3.x/site-packages など(ディストリビューションによって異なります)。

  6. 実態は pip install -e 時に指定したパスの絶対パスが記載されたテキストファイルです。

  7. zsh では [ ] は特殊文字であることに注意。