使用setup.py和软件包共享软件包版本的正确方法是什么?

时间:2013-07-11 01:04:35

标签: python setuptools distutils

使用distutilssetuptools等,在setup.py中指定了包版本:

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

我希望能够从包中访问相同的版本号:

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

我可以将__version__ = '1.0.0'添加到我的包的__init__.py中,但我还想在我的包中包含其他导入来创建包的简化界面:

# 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和软件包共享软件包版本的正确方法是什么?

9 个答案:

答案 0 :(得分:71)

仅在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

答案 1 :(得分:18)

我不相信这有一个规范的答案,但我的方法(直接复制或稍微调整我在其他地方看到的)如下:

文件夹层次结构(仅限相关文件):

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.
)

......这就像罪一样丑陋......但它确实有效,而且我已经在人们分发的软件包中看到它或类似的东西,如果有的话,我希望知道更好的方式。

答案 2 :(得分:11)

我同意@stefano-m 's philosophy关于:

  

拥有版本 =&#34; x.y.z&#34;在源代码中解析它   setup.py绝对是正确的解决方案,恕我直言。好多了   (反过来说)依靠运行时魔术。

这个答案来自@ zero-piraeus&#39; s answer。重点是不要在setup.py中使用导入,而是从文件中读取版本&#34;。

我使用正则表达式来解析__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:此official Python packaging document(及其mirror)描述了更多选项。它的第一个选择也是使用正则表达式。 (取决于你使用的确切正则表达式,它可能会或可能不会处理版本字符串中的引号。虽然通常不是一个大问题。)

PPS:fix in ADAL Python现已被移植到此答案中。

答案 3 :(得分:3)

__version__放入your_pkg/__init__.py,然后使用setup.py解析ast

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&lt; 3.4,请注意importlib.util.find_spec不可用。此外,importlib当然不能依赖setup.py的任何后退。{1}}。在这种情况下,请使用:

import os

file_path = os.path.join(os.path.dirname(__file__), PKG_DIR, '__init__.py')

答案 4 :(得分:2)

基于accepted answer和评论,这就是我最终做的事情:

档案: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

  • 仅限附加导入(对于简化的包接口) 如果项目实际已安装

  • ,则会发生

答案 5 :(得分:2)

/// Initialize the MIDI Sampler public override init() { super.init() enableMIDI() } /// Enable MIDI input from a given MIDI client /// This is not in the init function because it must be called AFTER you start AudioKit /// /// [snipped parameter comments for brevity] open func enableMIDI(_ midiClient: MIDIClientRef = AKMIDI().client, name: String = "MIDI Sampler") 上提出了几种方法。

答案 6 :(得分:1)

setuptools 46.4.0 添加了基本的抽象语法树分析支持,因此 setup.cfg attr: directive 无需导入包的依赖项即可工作。这使得拥有包版本的单一真实来源成为可能,从而使 setupstools 46.4.0 发布之前发布的先前答案中的许多解决方案过时。

现在可以避免将版本传递给 setup.py 中的 setuptools.setup 函数,如果 __version__ 在 yourpackage.__init__.py 中初始化并且以下元数据添加到包的 setup.cfg 文件中。使用此配置,setuptools.setup 函数将自动从 yourpackage.__init__.py 解析包版本,您可以在应用程序中需要的地方自由导入 __version__.py。

示例

setup.py 没有传递给 setup 的版本

from setuptools import setup

setup(
    name="yourpackage"
)

你的包.____init__.py

__version__ = 0.2.0

setup.cfg

[metadata]
version = attr: package.__version__

应用中的某个模块

from yourpackage import __version__ as expected_version
from pkg_distribution import get_distribution

installed_version = get_distribution("yourpackage").version

assert expected_version != installed_version

答案 7 :(得分:0)

接受的答案要求已安装软件包。就我而言,我需要从源__version__中提取安装参数(包括setup.py)。在查看tests of the setuptools package时,我找到了一个直接而简单的解决方案。要查找有关_setup_stop_after属性的更多信息,请引导我an old mailing list post提及distutils.core.run_setup,这会引导我the actual docs needed。毕竟,这是一个简单的解决方案:

档案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='flyingcircus@example.com',
      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()

答案 8 :(得分:-1)

很晚,我知道。但这对我有用。

module / version.py:

__version__ = "1.0.2"

if __name__ == "__main__":
    print(__version__)

模块/ __ init __。py:

from . import version
__version__ = version.__version__

setup.py:

import subprocess

out = subprocess.Popen(['python', 'module/version.py'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
stdout,stderr = out.communicate()
version = str(stdout)

对我来说,主要优点是不需要手工解析或正则表达式或manifest.in条目。它也是相当Pythonic的,似乎在所有情况下都可以工作(pip -e等),并且可以通过在version.py中使用argparse轻松扩展以共享文档字符串等。谁能看到这种方法的问题?