Poetryでsys.path.appendを回避する
まとめ
pyproject.tomlの[tool.poetry]のpackagesに以下のようにモジュールを追加しましょう。
packages = [
{ include = "hoge" },
{ include = "fuga" },
]
背景から詳しく
Pythonのモジュールシステムは他の言語と結構な違いがあり、どのファイルからどのモジュールをimportできるのかで頭を悩ませることが多々あります。
例えば、次のようなディレクトリ階層を持つプロジェクトについて考えてみましょう。
.
├── pyproject.toml
├── hoge
│ └── a.py
├── fuga
└── b.py
hoge/a.pyとfuga/b.pyはそれぞれ以下のようになっています。
print(__file__)
import hoge.a
# 何かを実行
ここで、プロジェクトルートにいる状況で、python fuga/b.pyを実行したいとします。
fuga/b.pyはhoge/a.pyをimportしていますが、通常、このままではimportに失敗します。
できればプロジェクトルートからのパスでimportできると複雑さを軽減できるのですが、そのためにはプロジェクトルートをsys.pathに追加してあげる必要があり、結果として以下のような美しくないプログラムが出来上がりがちです。
import sys
sys.path.append(".")
import hoge.a
# 何かを実行
この問題の原因はsys.pathにカレントディレクトリ(プロジェクトルート)が追加されておらず、hogeモジュールが見えないことが原因です。
この問題の解決法として、環境変数のPYTHONPATHを使うことができます。
PYTHONPATHはPythonがsys.pathに追加すべきパスのリストで(参考)、これを使うことで明示的なsys.path.appendを実行しなくてもsys.pathにカレントディレクトリを追加することができます。
PYTHONPATHを用いた場合のfuga/b.pyの実行方法は以下の通りです。
# `import sys` などは不要
import hoge.a
# 何かを実行
PYTHONPATH=.:$PYTHONPATH python fuga/b.py
これで煩雑なsys.path.appendを抑制することができましたが、今度はPythonコマンド実行のたびに環境変数を指定する必要が出てきてしまいました。
これでは面倒臭さが軽減されていません。
そこで、sys.pathにうまく追加するのではなく、別のツールを使って解決できないかを考えてみます。
そのような場合に使えるのが、Pythonのパッケージマネージャ 兼 依存関係管理用のツールであるPoetryです。
Poetryを用いることで、依存関係の管理や仮想環境の準備、パッケージのビルドなど様々な処理を簡単に実行できます。
今回はPoetryの設定ファイルを書くことで、これらのパッケージのimportをできるようにしてみます。
Poetryの設定にはpyproject.tomlの[tool.poetry]を使いますが、その中のpackagesというセクションが今回の場合に使えます。
このセクションはプロジェクトをPythonパッケージとして公開する際に使えるものですが、開発環境においてどのパッケージを仮想環境に入れるか、という用途でも使うことができます。
具体的には、今回の場合以下のように設定を行います。
[tool.poetry]
name = "..."
version = "x.y.z"
packages = [
{ include = "hoge" },
{ include = "fuga" },
]
この設定を記述した後、poetry installを実行します。
これにより、プロジェクトルートディレクトリがsys.pathに自動的に含まれるようになり、結果として先ほどの例を問題なく実行できるようになります。
Poetryを用いてPythonを実行するには、以下のようにすればよいです。
import hoge.a
# 何かを実行
poetry run python fuga/b.py
すると、実行結果は以下のようになります。
/path/to/hoge/a.py
fuga/b.py
無事、importが成功し、実行できていることがわかりました。
考察
さて、ここまでPoetryのpackagesセクションを用いてこのような動作を成功させましたが、実はちょっと不思議な挙動をしています。
時間がある方は、以下のようにpackagesに片方だけを記述し、.venvを削除したのちにpoetry installをして同様の実験をしてみてください。
[tool.poetry]
name = "..."
version = "x.y.z"
packages = [
{ include = "fuga" },
]
おそらく、poerty run python fuga/b.pyは成功すると思います。
なぜ、hogeが追加されていない(何にかはよくわからない)のにもかかわらず、実行に成功したのでしょうか。
これは、packagesに記載したincludeの親ディレクトリがsys.pathに追加されるためです。
packagesにはincludeのほかにfromという設定を記述することができ、実際にはfromのディレクトリがsys.pathに追加されていそうです(ちゃんと裏をとっていないので、訂正があればお教えいただけると助かります)。
したがって、このように片方だけをpackagesに記載したとしても、プロジェクトルートがsys.pathに追加されるので、問題なくhogeを見つけることができる、というわけです。
なので、例えばプロジェクトルートにsrcがあるような一般的なPythonプロジェクトの構成であれば以下のように1行追加するだけでよい可能性が高いです。
[tool.poetry]
name = "..."
version = "x.y.z"
packages = [
{ include = "src" },
]
これらの設定をどのように記述するべきかはプロジェクトによって変化しそうなので、もし「このケースではこうする必要があった」という話があれば、他の方のためにもぜひこの記事のコメント欄に書いていただけると幸いです。
この記事は以上になります。
ここまで読んでいただきありがとうございました。
みなさんもよきPython生活を。
Discussion