Pythonモジュールを配布する場合、そのバージョン番号を記述する必要がありますが、
-
pip installした後にpip listで参照出来るように、setup.pyのsetup(version='')に記述 -
import mylibraryした後にmylibrary.__version__で参照出来るように、mylibrary/__init__.pyに記述
の2つの記述が必要になります。
当然、同じバージョン番号を2箇所に書くのは手間、かつ間違いの元なので、1箇所で済ませたい所です。
__version__.pyにバージョン番号を記述し、
__version_info__ = (1, 0, 0)
__version__ = '.'.join(map(str, __version_info__))
setup.pyと__init__.pyからimport __version__するという構成は、よくあるパターンだと思います。
__version__の罠
この構成には問題があります。
__init__.py(もしくは__init__.pyでimportしているコアモジュール)に、
非標準モジュールのimportを記述した場合です。
from __version__ import __version__
from core import MyClass # mylibrary.MyClassで使えるようにするためのショートカット
import numpy as np # MyClassで必要な外部モジュールのimport
class MyClass(object):
pas
この状態でsetup.pyを実行すると、、、
> py setup.py install
Traceback (most recent call last):
File "C:\Users\hoge\git\example\setup.py", line 4, in <module>
from mylibrary import __version__
File "C:\Users\hoge\git\example\mylibrary\__init__.py", line 2, in <module>
from .core import MyClass
File "C:\Users\hoge\git\example\mylibrary\core.py", line 1, in <module>
import numpy as np
ImportError: No module named numpy
ImportErrorとなり、mylibraryモジュールのインストールが出来なくなります。
numpyをインストールするためのinstall_requiresがsetup.pyに記述してあるにも関わらず、
そのsetup.pyが実行出来ずにインストール出来ないという、「金庫の中の鍵」状態です。
ベストだったプラクティス1
原因は__version__.pyをimportすると、同じディレクトリの__init__.pyも一緒にimportされてしまう為です。
従って、解決策は「importせずに__version__.pyを読み込む」です。
# !/usr/bin/python
from setuptools import setup, find_packages
# from __version__ import __version__ # 削除、ImportErrorの原因
import os
packages = find_packages()
ns = dict()
for package in packages:
version_file = os.path.join(package, '__version__.py')
if os.path.exists(version_file):
with open(version_file, mode='rt') as f:
eval(compile(f.read(), version_file, 'exec'), dict(), ns)
break
__version__ = ns['__version__']
del ns
setup(
version=__version__,
)
詳しく解説はしませんが、__version__.pyを探し、evaluateして、変数__version__を直接取り出しています。
この方法で、ImportErrorを引き起こす__init__.pyのimportを回避することが出来ます。
とは言え、
setup.pyにメタデータ以外のコードを書きすぎるのは抵抗があるし、
__init__.pyで非標準モジュールをimportしたらダメというのも現実的ではありません。
「バージョン番号の記述を1箇所にまとめようとしたらエラーになった」みたいな事がが容易に起こり得るようでは、
「__version__はアンチパターン」という意見が出るのも頷けます。
ベストプラクティス
setup.pyをこう記述してください。
from setuptools import setup
setup()
見ての通り、setup()を呼び出すだけで、設定は空っぽです。
setup()は中身が空かどうかに関わらず、setup.cfgファイルがあれば、そこから不足している設定を取得します。
そして、setup.cfgにはファイルと変数を指定して参照する記述方法があります。
[metadata]
version = attr: mylibrary.__version__.__version__
これでsetup()のversionに、ファイルmylibrary/__version__.pyの変数__version__を適用したのと同じ効果があります。
ところで、この方法でも、__version__.pyをimportするのと同じ様に、
-
__init__.pyもimportされるのではないか? - そしてImportErrorになるのではないか?
という気がしますが、setup.cfgに記述した変数の参照は「ベストではないプラクティス」のsetup.pyと同じように、
importの仕組みを通さずにファイルを直接eval()するようで、ImportErrorにはなりません。
まとめ
version以外にもsetup.pyに記述していた大抵の事は、setup.cfgにスマートに記述できます。
また、setup()のメタデータだけでなくflake8やpy.test、nosetestなどの設定も一緒に記述することが出来、
ディレクトリを綺麗に保つことにも役立ちます。
せっかくsetup.cfgを置くのだから、最大限活用して行きたいですね。
-
本当はこれを「ベストプラクティス」として今年のAdventCalendarを書くつもりでしたが、直前になって圧倒的に簡単な方法が見つかったので、まるっとボツになりました。悔しいのでそのまま置いておきます。
setup.cfgが使えないような超絶古い環境を使っている人は参考にしてください。 ↩