__version__の罠とベストプラクティス

Pythonモジュールを配布する場合、そのバージョン番号を記述する必要がありますが、

* `pip install`した後に`pip list`で参照出来るように、`setup.py`の`setup(version='')`に記述
* `import mylibrary`した後に`mylibrary.__version__`で参照出来るように、`mylibrary/__init__.py`に記述

の２つの記述が必要になります。
当然、同じバージョン番号を2箇所に書くのは手間、かつ間違いの元なので、1箇所で済ませたい所です。

`__version__.py`にバージョン番号を記述し、

```py:mylibrary/__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を記述した場合です。

```py:mylibrary/__init__.py
from __version__ import __version__
from core import MyClass  # mylibrary.MyClassで使えるようにするためのショートカット
```

```py:mylibrary/core.py
import numpy as np  # MyClassで必要な外部モジュールのimport

class MyClass(object):
    pas
```

この状態で`setup.py`を実行すると、、、

```CONSOLE:突然のImportError
> 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]

[^1]: 本当はこれを「ベストプラクティス」として今年のAdventCalendarを書くつもりでしたが、直前になって圧倒的に簡単な方法が見つかったので、まるっとボツになりました。悔しいのでそのまま置いておきます。`setup.cfg`が使えないような超絶古い環境を使っている人は参考にしてください。

原因は`__version__.py`をimportすると、同じディレクトリの`__init__.py`も一緒にimportされてしまう為です。
従って、解決策は「importせずに`__version__.py`を読み込む」です。

```py:setup.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`をこう記述してください。

```py:setup.py
from setuptools import setup
setup()
```

見ての通り、`setup()`を呼び出すだけで、設定は空っぽです。

`setup()`は中身が空かどうかに関わらず、`setup.cfg`ファイルがあれば、そこから不足している設定を取得します。
そして、`setup.cfg`にはファイルと変数を指定して参照する記述方法があります。

```INI: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`を置くのだから、最大限活用して行きたいですね。