[Python] 最小限setup.pyでのビルドを通じてsetuptoolsの気持ちを聞いてみた

2023.04.10

Glueを使っていて、Pythonの自作スクリプトを使用するためにwheelファイルを作成しています。 これをきっかけに、setup.pyファイルの記述内容について理解しておこうと思いました。

こういった時は私はいつも最小限のコードを作ることから始めています。 最小限のコードで動かすと、 そのライブラリや仕組みから「こういう使い方がデフォルトなんだよ!」という声を聞くことができます。 やたらと省略可能引数をたくさんつけて実行するということは、 それだけ元から想定されていたデフォルトから外れたことをしているんだと気づくための指標になります。 (必ずしもそうではない場合もあることは承知していますが)

ということで今回はsetup.pyの記述を最小限にした時の挙動確認をやってみました。

検証環境

  • Python 3.11.2

最小限の構成

hogeモジュールをもつapp_commonパッケージを作ろうと思います。 ディレクトリ構成はこんな感じです。

$ tree .
.
├── app_common
│   ├── __init__.py
│   └── hoge.py
└── setup.py

__init__.pyはパッケージには必ず必要なものになりますので、 中身は空で良いので配置する必要があります。

最小限のsetup.pyはこんな感じです。

setup.py

from setuptools import setup

setup()

setup()を呼んでいるだけですね、はい。 これの実行方法は

$ python setup.py bdist_wheel

です。 bdist_wheelは、出力としてビルドされたwheel形式のものを出すという意味です。 ちなみにsetup()に明示的に渡されていないからちょっと気持ち悪いですが、 setup()の中でsys.argvが使われています。

ビルドした後のカレントディレクトリの様子はこんな感じです。

$ tree
.
├── app_common
│   ├── __init__.py
│   └── hoge.py
├── app_common.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   └── top_level.txt
├── build
│   ├── bdist.macosx-12-arm64
│   └── lib
│       └── app_common
│           ├── __init__.py
│           └── hoge.py
├── dist
│   └── app_common-0.0.0-py3-none-any.whl
└── setup.py

whlファイルの正体はzipファイルなので、unzipして中身を見るとこうなっています。

├── app_common
│  ├── __init__.py
│  └── hoge.py
└── app_common-0.0.0.dist-info
    ├── METADATA
    ├── RECORD
    ├── WHEEL
    └── top_level.txt

app_commonディレクトリの中身がそのまま入っていることがわかります。 whlファイルがsys.pathに配置されている場合、 このディレクトリがimport対象として記述できることになります。

最小限の時の挙動

カレントディレクトリにあるパッケージをビルドします。 具体的には、カレントディレクトリ直下の__init__.pyを含むディレクトリを探し、 それをPythonパッケージと見なしてパッケージングします。

app_commonというディレクトリをパッケージングした時の 出来上がるwheelファイルはこうなっています。

app_common-0.0.0-py3-none-any.whl

setupに特に何も指定していないので、

  • パッケージ名はディレクトリと同じ
  • バージョンは0.0.0

で作られることがわかります。

今回はピュアPythonを想定していますので、ABIタグ(noneになっている部分)やアーキテクチャ(anyになっている部分)については特に触れません (嘘です、私がそれほど詳しく知りません)が、広くほとんどの環境で動くものとなります。

パッケージングの際の備考

__init__.py

パッケージングされるディレクトリは __init__.pyを含む必要があります。 正確には、含まなくてもwheelファイルの生成自体は成功しますが、 中にあるhoge.pyはwheelには含まれませんので、やりたいことは全く達成できていません。

ディレクトリ名

ディレクトリ名がそのままパッケージ名になりますので、 パッケージ名として使えない文字が入っているディレクトリは無視されるようです。 例えばapp_common.tmpは対象となりません。

ディレクトリの数

この最小限指定では、パッケージングできるディレクトリは一つだけです。 もし複数のディレクトリがパッケージングできる条件を満たしていた場合、 エラーが出てビルドが失敗します。

setupの引数

setupの動きを知る上での最低限の引数だけ少し確認してみます。

name

パッケージの名前を指定します。 ディレクトリと違う名前をつけたい時は指定します。 というか、常に指定した方がいいでしょう。

あくまでもパッケージ名なので、 例えばpackage_nameなんて名前をつけたとしても、 このパッケージをimportする場合は

# import package_name ではなく
import app_common

となります。

packages

ライブラリに含めるパッケージを指定します。 デフォルトでは1つのパッケージしか含めることができませんでしたが、 ここで引数を指定することで複数パッケージを含めることができます。

これを指定すれば複数パッケージを含められるのであれば、 「指定しなくても自動的に見つけたディレクトリを全部含めてくれればいいのに」と思わなくもないですね。 一つのwheelファイルに複数のパッケージを入れることは可能という事実と、 どのパッケージを入れるのかを明示的に書くことが自然であることから、 これも必須で指定した方が良さそうです。

なお、nameを指定しないでpackagesを指定した場合は、 パッケージ名はUNKNOWNとなるようです。

include_package_data

通常、ビルドした成果物には.pyなどの一部のファイルしか格納されません。 (含まれる物としては MANIFEST.in を使ってソースコード配布物にファイルを含める を参照してください。)

MANIFEST.inファイルに、含めるファイルを定義し、 include_package_data=Trueを指定することで自由にファイルを含めることができます。 MANIFESTファイルの書き方についてはやはり上記公式ドキュメントを参照して欲しいのですが、 ポイントとしては、MANIFESTファイルの記述に加えてinclude_package_data=Trueの指定も必要ということです。 想定されたファイル形式以外はライブラリに含めたくないという意思を感じますね。

まとめ

setup.pyの記述を最小限にした際の挙動についてまとめてみました。

無理に最小限にする必要はない(特にnameとか)ですが、 デフォルトの動きがわかると、そのメソッドなどがどういう思想で作られているのかがわかるので、 それを知った上で利用すると理解が深まりますし、納得感があって良いですね!

誰かのお役に立てば幸いです!