web-dev-qa-db-ja.com

setup.pyとパッケージでパッケージバージョンを共有する正しい方法は何ですか?

distutilssetuptoolsなどを使用すると、setup.pyでパッケージバージョンが指定されます。

# file: setup.py
...
setup(
name='foobar',
version='1.0.0',
# other attributes
)

パッケージ内から同じバージョン番号にアクセスできるようにしたい:

>>> import foobar
>>> foobar.__version__
'1.0.0'

パッケージの__init__.pyに__version__ = '1.0.0'を追加できましたが、パッケージに追加のインポートを含めて、パッケージへの単純化されたインターフェースを作成したいと思います。

# file: __init__.py

from foobar import foo
from foobar.bar import Bar

__version__ = '1.0.0'

そして

# file: setup.py

from foobar import __version__
...
setup(
name='foobar',
version=__version__,
# other attributes
)

ただし、これらの追加のインポートにより、まだインストールされていない他のパッケージをインポートすると、foobarのインストールが失敗する可能性があります。 setup.pyとパッケージでパッケージバージョンを共有する正しい方法は何ですか?

58
Jace Browning

バージョンをsetup.pyのみに設定し、独自のバージョンを pkg_resources で読み取り、setuptoolsメタデータを効率的に照会します。

ファイル:setup.py

setup(
    name='foobar',
    version='1.0.0',
    # other attributes
)

ファイル:__init__.py

from pkg_resources import get_distribution

__version__ = get_distribution('foobar').version

これをインストールせずに実行できるすべての場合にこれを機能させるには、DistributionNotFoundと配布場所をテストします。

from pkg_resources import get_distribution, DistributionNotFound
import os.path

try:
    _dist = get_distribution('foobar')
    # Normalize case for Windows systems
    dist_loc = os.path.normcase(_dist.location)
    here = os.path.normcase(__file__)
    if not here.startswith(os.path.join(dist_loc, 'foobar')):
        # not installed, but there is another version that *is*
        raise DistributionNotFound
except DistributionNotFound:
    __version__ = 'Please install this project with setup.py'
else:
    __version__ = _dist.version
72
Martijn Pieters

私はこれに正統的な答えがあるとは思わないが、私の方法(直接コピーするか、他のさまざまな場所で見たものから少し調整した)は次のとおりです:

フォルダー階層(関連ファイルのみ):

package_root/
 |- main_package/
 |   |- __init__.py
 |   `- _version.py
 `- setup.py

main_package/_version.py

"""Version information."""

# The following line *must* be the last in the module, exactly as formatted:
__version__ = "1.0.0"

main_package/__init__.py

"""Something Nice and descriptive."""

from main_package.some_module import some_function_or_class
# ... etc.
from main_package._version import __version__

__all__ = (
    some_function_or_class,
    # ... etc.
)

setup.py

from setuptools import setup

setup(
    version=open("main_package/_version.py").readlines()[-1].split()[-1].strip("\"'"),
    # ... etc.
)

...これは罪のようにいです...しかしそれは機能しますし、もしあればもっと良い方法を知りたいと思っている人々によって配布されたパッケージでそれやそのようなものを見てきました。

18
Zero Piraeus

@ stefano-mの哲学 について同意します:

ソースにversion= "x.y.z"を設定し、setup.py内で解析することは間違いなく正しい解決策です。ランタイムマジックに依存するよりもはるかに優れています。

そして、この答えは@ zero-piraeusの answer から派生しています。要点は、「setup.pyでインポートを使用せず、代わりにファイルからバージョンを読み取る」ことです。

正規表現を使用して__version__を解析し、専用ファイルの最後の行にする必要がないようにします。実際、プロジェクトの__version__の中に、単一の真実のソース__init__.pyを配置しています。

フォルダ階層(関連ファイルのみ):

package_root/
 |- main_package/
 |   `- __init__.py
 `- setup.py

main_package/__init__.py

# You can have other dependency if you really need to
from main_package.some_module import some_function_or_class

# Define your version number in the way you mother told you,
# which is so straightforward that even your grandma will understand.
__version__ = "1.2.3"

__all__ = (
    some_function_or_class,
    # ... etc.
)

setup.py

from setuptools import setup
import re, io

__version__ = re.search(
    r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]',  # It excludes inline comment too
    io.open('main_package/__init__.py', encoding='utf_8_sig').read()
    ).group(1)
# The beautiful part is, I don't even need to check exceptions here.
# If something messes up, let the build process fail noisy, BEFORE my release!

setup(
    version=__version__,
    # ... etc.
)

...これはまだ理想的ではありません...

ところで、この時点で、この方法で新しいおもちゃをテストできます。

python setup.py --version
1.2.3

PS:この 公式Pythonパッケージングドキュメント (およびその ミラー )には、より多くのオプションが記載されています。最初のオプションも正規表現を使用しています。使用する正確な正規表現。バージョン文字列内の引用符を処理する場合もしない場合もあります。一般的には大きな問題ではありません。)

PPS: ADAL Pythonの修正 は、この回答にバックポートされました。

12
RayLuo

__version__ in your_pkg/__init__.py、およびsetup.pyastを使用:

import ast
import importlib.util

from pkg_resources import safe_name

PKG_DIR = 'my_pkg'

def find_version():
    """Return value of __version__.

    Reference: https://stackoverflow.com/a/42269185/
    """
    file_path = importlib.util.find_spec(PKG_DIR).Origin
    with open(file_path) as file_obj:
        root_node = ast.parse(file_obj.read())
    for node in ast.walk(root_node):
        if isinstance(node, ast.Assign):
            if len(node.targets) == 1 and node.targets[0].id == "__version__":
                return node.value.s
    raise RuntimeError("Unable to find version string.")

setup(name=safe_name(PKG_DIR),
      version=find_version(),
      packages=[PKG_DIR],
      ...
      )

Python <3.4を使用する場合、 importlib.util.find_spec は使用できません。さらに、importlibのバックポートはもちろん、setup.py。この場合、次を使用します。

import os

file_path = os.path.join(os.path.dirname(__file__), PKG_DIR, '__init__.py')
3
nexcvon
2
funky-future

受け入れられた答え とコメントに基づいて、これは私がやったことです:

ファイル:setup.py

setup(
    name='foobar',
    version='1.0.0',
    # other attributes
)

ファイル:__init__.py

from pkg_resources import get_distribution, DistributionNotFound

__project__ = 'foobar'
__version__ = None  # required for initial installation

try:
    __version__ = get_distribution(__project__).version
except DistributionNotFound:
    VERSION = __project__ + '-' + '(local)'
else:
    VERSION = __project__ + '-' + __version__
    from foobar import foo
    from foobar.bar import Bar

説明:

  • __project__はインストールするプロジェクトの名前で、パッケージの名前とは異なる場合があります

  • VERSIONは、--versionが要求されます

  • プロジェクトが実際にインストールされている場合にのみ、追加のインポート(簡易パッケージインターフェイス用)が発生します。

2
Jace Browning

受け入れられた回答では、パッケージがインストールされている必要があります。私の場合、ソース__version__からインストールパラメーター(setup.pyを含む)を抽出する必要がありました。 setuptoolsパッケージのテスト を見ていると、直接かつ簡単な解決策が見つかりました。 _setup_stop_after属性に関する詳細情報を探すと、 古いメーリングリストの投稿 につながり、distutils.core.run_setupに言及しました 実際に必要なドキュメント 。結局のところ、ここに簡単な解決策があります。

ファイルsetup.py

from setuptools import setup

setup(name='funniest',
      version='0.1',
      description='The funniest joke in the world',
      url='http://github.com/storborg/funniest',
      author='Flying Circus',
      author_email='[email protected]',
      license='MIT',
      packages=['funniest'],
      Zip_safe=False)

ファイルextract.py

from distutils.core import run_setup
dist = run_setup('./setup.py', stop_after='init')
dist.get_version()
2
ZachP