前回は開発環境としてJupyterLabを使う話を書いた。
今回はプロジェクトの構成について。
プロジェクト構成
RubyであればBundlerで標準化されているのでプロジェクト構成をどうしようとか迷う必要はないんだけど、Pythonはそうではないので迷う。 ちょっと調べるだけで複数の構成が出てくるんだけど、一般的なプロジェクト構成はおそらく以下のようになる:
- (プロジェクトルート)/
なお、プロジェクトルートの直下にパッケージルートを置かずにsrc/
を一段挟む構成もあって、個人的にはそちらの方がいいと思うんだけど(examples/
やtests/
をsrc/
以下と同じディレクトリ構成にできるので統一感がある)、Pythonではプロジェクトルート直下にパッケージルートを置くのが一般的なようなので、悩ましいところ。
pipパッケージとしてビルド可能にする
さて、プロジェクトルート直下にpyproject.toml
、setup.py
、setup.cfg
というファイルが置かれているけど、これはpipパッケージとしてビルド可能にするためのもの。
ライブラリとして使わないのであれば不要と思うかもしれないけど、これはあった方がいい。
というのも、以下のようなメリットがあるから:
- 依存するパッケージを賢く指定できる
- パスを気にしなくてよくなる
- コマンドラインツールを簡単に作れる
依存するパッケージを賢く指定できる
たとえば、ある依存ライブラリについて、開発時には必要だけど実際に使うときには不要ということがある。 そういう場合、状況に合わせてインストールする依存ライブラリを切り替えたい。
けど、よくあるrequirements.txt
による管理ではそれは難しい。
そこで、公式でない仮想環境のpipenvを導入してる人もいるけど、実はこれはsetup.cfg
をちゃんと書けば実現できる。
もちろんsetuptoolsも公式なツールではないけど、事実上の標準といえるので、setuptoolsを使った方がいい。
ちなみに、この方法を使うならrequirements.txt
を使う必要はまったくなくなる。
むしろrequirements.txt
は何も考えないと依存関係で入ったパッケージまでも並べられてしまって、本当に必要なパッケージ(場合によってはバージョンを固定したい)とそうではないパッケージ(バージョンは依存関係で定まる有効なもので最新なものであればいい)が区別がつかなくて、時間が経つごとに害悪度が増していくので、使わない方がいい。
パスを気にしなくてよくなる
また、コードを理解しやすくするためにサンプルコードを置くのはよくあるけど、このときにちょっと面倒なのがパスを追加すること。 ちゃんとパスを通してやらないとimportがうまくいかずにコードが動いてくれない。
けど、これもpipパッケージとしてビルド可能にして開発用としてインストールすることで、勝手にパスが通ってくれるようになる。
コマンドラインツールを簡単に作れる
さらに、setup.cfg
を書くことでコマンドラインツールを簡単に作ることもできる。
せっかく書いたコードだから簡単に使えるといいわけだけど、普通これはなかなか大変。 それが設定を書くだけで使えるようになるんだから、使わない手はない。
pyproject.toml
pyproject.toml
の内容は以下:
[build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta"
これに加えて後述するpysenの設定を書くといい。
setup.py
setup.py
の内容は以下:
from setuptools import setup setup()
「これだけ?」と思うかもしれないけど、これは設定をsetup.cfg
に書くように変わったから。
ただの設定なんだから設定ファイルで指定すればいいのは当たり前なわけで。
いまだにアホのような数の引数をsetup()
に渡しているセンスのないコードが巷には溢れかえってるけど。。。
setup.cfg
setup.cfg
ではパッケージの設定を書いていくことになる。
いろいろ設定はあるんだけど、基本的なものを書くと以下のようになる:
[metadata] name = <pipパッケージ名> version = <pipパッケージのバージョン> description = <pipパッケージの簡単な説明> license = <ライセンス> [options] packages = <pipパッケージに含めるパッケージ1> <pipパッケージに含めるパッケージ2> ... install_requires = <依存するpipパッケージ1> <依存するpipパッケージ2> ... [options.extras_require] <条件名1> = <条件名1で追加で依存するpipパッケージ1> <条件名1で追加で依存するpipパッケージ2> ... <条件名2> = <条件名2で追加で依存するpipパッケージ1> <条件名2で追加で依存するpipパッケージ2> ... [options.entry_points] console_scripts = <コマンド名1> = <モジュール>:<main関数> <コマンド名2> = <モジュール>:<main関数> ...
「pipパッケージに含めるパッケージ」というのは、たとえばhoge
とhoge.huga
を指定すると、プロジェクトのhoge/
とhoge/huga/
の下にあるソースがpipパッケージに含むべきソースとして含まれることになる。
「依存するpipパッケージ」は、たとえばnumpyが必要ならnumpy
と書けばいいし、バージョンを指定したいならnumpy == 1.21
やnumpy >= 1.19, < 1.20
といった感じで書くといい。
「条件名」というのはdevelop
やtest
といった任意の名前で、pipパッケージをインストールするときに後ろに[<条件名>]
をつける(例: pip install hoge[develop]
やpip install hoge[develop, test]
)と、指定された条件名で依存するpipパッケージも追加でインストールされるようになる。
コマンドラインツールのは、たとえばhoge.app
モジュールでmain()
関数を定義していて、do_hoge
コマンドでその関数が呼び出されるようにしたければ、do_hoge = hoge.app:main
といった感じに指定しておけばいい。
これだけでコマンドが作れるので便利。
(引数はsys.argv
から取得する)
他にも、ソース以外のファイル(csvなど)をpipパッケージに含める方法などもある。 詳細は以下を参照:
pipパッケージのビルド、インストール
こうして準備したものをpipパッケージとしてビルドするには、以下のようにする:
# setuptools, wheel, buildをPython仮想環境にインストールしておく $ pip install setuptools wheel build # ビルドを実行 $ python -m build
ビルドが終わるとdist/
の下にwheelファイルが作られる。
これをインストールするには以下のようにすればいい:
$ pip install <wheelファイル>
なお、開発中は何度もビルドしていると大変。 そこで、ビルドはせずに以下のようにインストールすると、ローカルの変更がすぐに反映されていい:
# pipパッケージとしてインストールするけどソースは開発中のものがそのまま使われる $ pip install -e .
ちなみにインストールすると依存するpipパッケージも一緒にインストールされる。
さらにoptions.extras_require
で条件名が設定されてるなら、それを指定することで開発中に追加で必要な依存するpipパッケージをインストールすることもできる:
# options.extras_requireで条件名としてdevelopがあり、追加でインストールする例 $ pip install -e .[develop]
Linter, Formatter
さらに、チームで開発する場合、LinterやFormatterを使った方がいい。
これについてはpysenがお手軽でよさそうだった:
pysenは複数のツールをとりまとめてくれるものになっている。
インストールしたい場合、単に
$ pip install "pysen[lint]"
とするか、あるいはsetup.cfg
のoptions.extra_require
で
[options.extra_require] develop = pysen[lint]
のように書いておいてインストールするといい。
そして設定はpyproject.toml
で行う。
以下は設定例:
[tool.pysen] version = "0.9" [tool.pysen.lint] enable_black = true enable_flake8 = true enable_isort = true enable_mypy = true line_length = 88 py_version = "py37" [[tool.pysen.lint.mypy_targets]] paths = ["."]
blackはFormatter、flake8はコーディング規約であるPEP8に違反しないかチェックするLinter、isortはimportの順番を並び替えてくれるFormatter、mypyは型ヒントから型チェックを行ってくれるLInterとなっている。
Linterでチェックを行いたい場合は以下を実行:
$ pysen run lint
Formatterでコードを整形したい場合は以下を実行:
$ pysen run format
今日の内容をまとめると、以下:
- プロジェクト構成
- パッケージルート、
examples/
、tests/
をプロジェクトルート直下に用意する
- パッケージルート、
- pipパッケージとしてビルド可能にする
pyproject.toml
、setup.py
、setup.cfg
を置いてビルド可能にする- pipパッケージとしての設定は
setup.cfg
に書く requirements.txt
を使わない- pipパッケージとしてビルドするにはsetuptools、wheel、buildをインストールして
$ python -m build
- ビルドしたパッケージをインストールするには
$ pip install <wheelファイル>
- 開発用としてインストールするには
$ pip install -e .
- 開発時にだけ必要な依存するpipパッケージは
setup.cfg
のoptions.extras_require
で指定しておく
- Linter、Formatterとしてpysenを使う
- インストールは
$ pip install "pysen[lint]"
もしくはoptions.extra_require
を使う - 設定は
pyproject.toml
に書く - linterを実行するには
$ pysen run lint
- formatterを実行するには
$ pysen run format
- インストールは
今日はここまで!