パッケージのバージョンを1箇所で管理する#

課題

setuptools 以外のビルドバックエンドについてこのページを更新すること。

プロジェクトのバージョン番号をたったひとつの真実の場所で管理するテクニックはたくさん存在しています:

  1. setup.py に書かれたファイルを読み込んでバージョンを得ましょう。( pip setup.py での) 例はこちら:

    import codecs
    import os.path
    
    def read(rel_path):
        here = os.path.abspath(os.path.dirname(__file__))
        with codecs.open(os.path.join(here, rel_path), 'r') as fp:
            return fp.read()
    
    def get_version(rel_path):
        for line in read(rel_path).splitlines():
            if line.startswith('__version__'):
                delim = '"' if '"' in line else "'"
                return line.split(delim)[1]
        else:
            raise RuntimeError("Unable to find version string.")
    
    setup(
       ...
       version=get_version("package/__init__.py")
       ...
    )
    

    注釈

    setuptools 46.4.0 のリリースでは、代わりに次のものを setup.cfg ファイルに置くことで同じことを達成できています ("package" をパッケージをインポートする際の名前で置き換えてください) :

    [metadata]
    version = attr: package.__version__
    

    setuptools 61.0.0 のリリースの時点では、プロジェクトの pyproject.toml ファイルの中にバージョンを動的に指定することができます。

    [project]
    name = "package"
    dynamic = ["version"]
    
    [tool.setuptools.dynamic]
    version = {attr = "package.__version__"}
    

    attr: ディレクティブを含む装飾的な設定指示子が、 setup.py 向けのパラメータとしてはサポートされていないことに注意してください。

  2. 外部のビルドツールを使う場合は、両方の場所(ロケーション)を更新できるようなもの、あるいは、両方のサイトから使える API を提供しているものを使いましょう。

    使えるかもしれない外部のビルドツールのリスト、ただし、順不同で、ここに挙げられていなくても使えるものがあるかもしれません: bump2version, changes, commitizen, zest.releaser.

  3. プロジェクト内にグローバル変数の __version__ に値を設定した専用のモジュール (例えば version.py) を作って setup.py からそれを読み取って exec で値を変数に取り込むと良いでしょう。

    version = {}
    with open("...sample/version.py") as fp:
        exec(fp.read(), version)
    # later on we use: version['__version__']
    

    このテクニックを使っている例: warehouse

  4. 単純に VERSION という名前のテキストファイルに値を書いておいて、 setup.py とプロジェクトのソースコードの両方から読み込みます。

    with open(os.path.join(mypackage_root_dir, 'VERSION')) as version_file:
        version = version_file.read().strip()
    

    このテクニックを使う利点は、Python に限定されたやり方ではないということです。どんなツールでもバージョン番号を読み取ることができます。

    警告

    このやり方を採用するなら、 VERSION ファイルがすべてのソースコードとバイナリの配布物に含まれているように気を付けてください (例えば MANIFEST.ininclude VERSION を追加しておくなど)。

  5. setup.py に値を保存して、プロジェクトのソースコードが importlib.metadata API を使ってその値を動作中に取得するようにしましょう。 (importlib.metadata は Python 3.8 で導入されていて、それより古いバージョンでは importlib-metadata プロジェクトとして利用可能になっています) インストール済みのプロジェクトのバージョン番号をこの API で取り込むには次のようにします:

    import sys
    
    if sys.version_info >= (3, 8):
        from importlib import metadata
    else:
        import importlib_metadata as metadata
    
    assert metadata.version('pip') == '1.2.0'
    

    importlib.metadata API が知っているのはインストールされたパッケージのメタデータだけであって、現在インポートされているソースコードについて知っているとは限らないことに注意してください。

    あるプロジェクトで動作中にバージョン番号を取得する方法を使っているのであれば、Python 3.8 よりも古いバージョンを使う場合はそのプロジェクトの install_requiresimportlib-metadata を書いておかなければなりません:

    setup(
        ...
        install_requires=[
            ...
            'importlib-metadata >= 1.0 ; python_version < "3.8"',
            ...
        ],
        ...
    )
    

    importlib.metadata に対するもっと古い (かつ、より非効率な) 代替策は、 setuptools が提供する pkg_resources API です:

    import pkg_resources
    assert pkg_resources.get_distribution('pip').version == '1.2.0'
    

    あるプロジェクトで自分自身のバージョン番号を動作中に取得するために pkg_resources を使っているなら、プロジェクトの install_requires のリストには必ず setuptools が入っていないといけません。

    このテクニックを使っている例: setuptools.

  6. sample/__init__.py__version__ に値を設定して、 setup.py から sample をインポートしましょう。

    import sample
    setup(
        ...
        version=sample.__version__
        ...
    )
    

    警告

    このテクニックはよく知られたものだが、 install_requires で定義された依存先パッケージを sample/__init__.py がインポートしている場合には、そのようなパッケージは setup.py が実行される時点ではまだインストールされていない可能性が高いので、失敗するであろうということに注意してください。

  7. ソースコード内ではなくバージョンコントロールシステム (Git, Mercurialなど) のタグの中にバージョン番号を保持して、そこから setuptools_scm を使って取り出しましょう。