pythonでモジュールにversion情報を含めたい場合の方法のメモ
個人的には、pythonでパッケージを公開するときに、モジュールにバージョン情報を含めるのが好きではなかった。setup時のimportエラーなどが起きうる可能性を考えて。
ただ、たまたま要望されたのでその方法を調べてみたところ、手軽に扱えるようになっていた。これなら使っても良いかもしれない。
setup.cfgでattr:を使う
いろいろ調べた結果、setup.cfgでattr:
でバージョン情報を載せるのが一番良さそう。
例えば以下の様なファイルの構成で _version.py
に __version__
を載せるときには以下の様にすればOK。
foo/ ├── __init__.py ├── _version.py └── tests └── __init__.py 1 directory, 3 files
__version__.py
__version__ = "0.0.0"
__init__.py
from ._version import __version__ # noqa:F401
setup.cfg
# see: https://setuptools.readthedocs.io/en/latest/setuptools.html#specifying-values [metadata] name = foo version = attr: foo._version.__version__ # その他の設定は省略
attr: foo._version.__version__
が大切。かつてはだめだったけれど、すでに解決されていたのでこれでOK。
詳細
ここからは詳細。元々どのような問題が起きるかなどをメモしておく。
setup時にimportされてModuleNotFoundError
再現するrepositoryは以下(新しいsetuptoolsでは再現しないが)。
https://github.com/kohr-h/minimal
詳しい話題は以下のissueで。
https://github.com/pypa/setuptools/issues/1724
少しだけ真面目に解説すると、以下の様なファイル構成になってるときに、 __init__.py
で mod.py
をimportし、そこでnumpyなど何らかのモジュールをimportするような構成になっていたときに、setup.cfgで attr: minimal.__version__
と記述していたときにエラーになっていた。
. ├── minimal │ ├── __init__.py │ ├── _version.py │ └── mod.py ├── setup.cfg └── setup.py 1 directory, 5 files
$ pip install -e . # or python setup.py develop Traceback (most recent call last): File "setup.py", line 3, in <module> setup() File "/home/hkohr/miniconda/envs/tmp/lib/python3.7/site-packages/setuptools/__init__.py", line 145, in setup ... File "/home/hkohr/git/minimal/minimal/__init__.py", line 2, in <module> from . import mod File "/home/hkohr/git/minimal/minimal/mod.py", line 1, in <module> import numpy ModuleNotFoundError: No module named 'numpy'
このときのファイルは以下のようなもの。
__init__.py
from ._version import __version__ from . import mod
mod.py
import numpy
setup.cfg
[metadata] name = minimal version = attr: minimal.__version__
かつての実装ではsetup.pyで以下の様なコードを書いたものと同じだったため。
setup.py
from setuptools import setup from minimal._version import __version__ setup(version=__version__, ...)
pythonのモジュールの読み込み方は、下位のモジュールを読み込んだタイミングで、上位のモジュールも読み込まれるので、仮にminimal._version
をimportしようとしても、その前に minimal
がimportされる。そしてminimal
経由でminimal.mod
が読み込まれてしまう。
かつてのwork-around
work aroundとして以前は以下のように VERSIONというファイルを用意して、setup.cfgではこれを読み込む形にしていた。
version = file: VERSION
VERSIOIN
0.0.0
このとき、VERSIONと言うファイルを書き換えれば、pypiにアップロードされるバージョンを変更できるという形になっていたのだけれど、この方法ではモジュールにバージョン情報をもたせたいときに、何らかの方法でpythonのモジュール側のコードと同期させる手順が必要になった。これがめんどくさくてやっていなかった。
方法自体は以下の様な2種類の方法がある
- pythonのパッケージにVERSIONファイルが含まれるようにして、importlib.resourceなどで取り出す
- 何らかのビルド時のpre-processとして、VERSIONファイルの情報をコードに転写する処理を加える
どちらも手間がかかってめんどくさい。そもそもバージョン情報などが知りたかったら pip freeze
でもすれば良いじゃんと言う立場だった。
変更後
この処理がsetuptoolsのv46.4.0から変更されている。ファイルを直接読み込んでASTベースで情報を取得するように変更された。
実際どのようなコードになっているかと言うと以下の様な感じ。このStaticModuleというクラスを介して値を取得するように変更された。例えば、このStaticModuleの値mに対して、m.version
などとアクセスしたときには、構文上の代入部分(assign)を取り出してその値を返す。
setuptools/config.py
import ast class StaticModule: """ Attempt to load the module by the name """ def __init__(self, name): spec = importlib.util.find_spec(name) with open(spec.origin) as strm: src = strm.read() module = ast.parse(src) vars(self).update(locals()) del self.self def __getattr__(self, attr): try: return next( ast.literal_eval(statement.value) for statement in self.module.body if isinstance(statement, ast.Assign) for target in statement.targets if isinstance(target, ast.Name) and target.id == attr ) except Exception as e: raise AttributeError( "{self.name} has no attribute {attr}".format(**locals()) ) from e
そんなわけで、 version = attr: <module>._version.__version__
で良い。
参考
- https://github.com/pypa/setuptools/issues/1724
- https://stackoverflow.com/questions/58202909/modulenotfounderror-when-using-setup-cfg-and-version-accessed-with-attr
- https://setuptools.readthedocs.io/en/latest/history.html#id141
- https://github.com/pypa/setuptools/pull/1753