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


70

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


3
要维护版本号的单一事实来源,基本上可以执行5种常见模式
KF

1
我在这里有相关的答案stackoverflow.com/a/45656438/64313
cmcginty

Answers:


82

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

2
如果这确实可靠地起作用,那么它比我的回答要优雅得多……这使我想知道为什么我没有在其他地方看到它。有谁知道是否是一个真正的问题?如果确实报告了错误的版本号,那么它的优雅就在这里或那里……
零比雷埃夫斯

15
我不喜欢这种解决方案:__version__在运行时解决,而不是在构建时解决。恕我直言,我更喜欢__version__在源代码树中包含一个静态 代码,并在构建时使用一些代码(setup.py如下面的答案)读取它。
Stefano M

3
我的意思是__version__ = "x.y.z"setup.py在构建时由解析一次)和__version__ = some_weird_function()在运行时进行评估以恢复仅存在于setup.py和中的信息之间的差异foobar.egg-info
Stefano M

3
同意:我的措辞不正确,因为Python是一种解释语言。但是,重要的是要标记出在构建时(如果setup.py无法解析__version__ = 'x.y.z')或在运行时(如果get_distribution('foobar')无法恢复正确的信息)可能发生的故障之间的差异。您的方法当然具有很多优点,例如在构建时更改版本号:python setup.py build --tag-date。必须输入的内容__version__:某些东西烧入了源树,还是一些在构建时计算并在运行时恢复的元数据?
Stefano M

9
我必须同意@StefanoM这个解决方案不是最优的。我发现它具有误导性的原因是,如果系统上既有安装版本又有开发版本,则无论实际导入哪个版本,它都将始终显示已安装的版本。
greschd 2015年

22

我不相信对此有一个规范的答案,但是我的方法(直接复制或与我在其他地方看到的略有不同)如下:

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

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

……这很丑陋……但确实有效,而且我在人们希望分发的软件包中看到了它或类似的东西,我希望他们知道有更好的方法。


5
tl; dr:不要在setup.py中使用导入,请从文件中读取版本。我必须考虑一会儿才能决定我是否喜欢这种方法……
杰斯·布朗宁

1
@JaceBrowning是的,这是一个合理的总结……我怀疑任何解决方案都必须是此解决方案的一种变体,因为它在setup.py中导入了会导致问题的软件包。
比雷埃夫斯零(Zero Piraeus)

我不知道是否setuptoolsdistutils有功能更优雅地做到这一点?
杰斯·布朗宁

9
__version__ = "x.y.z"源和内解析它setup.py绝对正确的解决方案,恕我直言。更好地依靠运行时魔术。
Stefano M

1
另一种__version__定义方法setup.py是使用pkg_resources.resource_stringexec。例如:version_info = {}; version_txt = resource_string('my_package', 'foo.py'); exec(version_txt, version_info); print(version_info['__version__']
MPlanchard '16

17

我同意@ stefano-m的哲学

源码中有version =“ xyz”并在setup.py中进行解析绝对是正确的解决方案,恕我直言。比依靠运行时魔术更好(反之亦然)。

这个答案来自@ zero-piraeus的答案。重点是“不要在setup.py中使用导入,而是从文件中读取版本”。

我使用regex解析,__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中修复程序移植到此答案中。


1
关于不从setup.py导入任何程序包并手动分析版本的说法非常正确。
亚历克斯

3
这对我来说似乎也是最优雅的方法。感谢
Bede Constantinides

2
该链接似乎是这里(可能是)官方Python指南的一面镜子:Packaging.python.org/single_source_version
ibic

@ABB是的,我知道。试图使用描述性功能名称来揭示意图,对吗?在大多数情况下,通常这是一个好主意。但是这里不必教条。因为将值分配给众所周知的变量的模式__version__ = blah blah blah已经清楚地表明了其意图:将向该变量分配一些版本值。我会说这只是个人喜好。
RayLuo

谢谢@ibic。我已经用您提供的官方链接更新了答案。
RayLuo

2

__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 <3.4,请注意该importlib.util.find_spec功能不可用。而且,importlib当然不能依赖任何反向端口setup.py。在这种情况下,请使用:

import os

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

2

接受的答案要求已安装软件包。就我而言,我需要__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='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()

您运行哪个文件来构建可分发文件?
变量

您将路径setup.py传递到run_setup,该路径开始从setup.py安装软件包,但stop_after = init导致它在实际安装任何组件之前停止。
ZachP

这很棒。我确实想要@ZachP似乎要关注的问题。我想要setuptools方法中应该存在的版本setup。这似乎是唯一使您能够从方法中的version变量获取版本setup而不stop_after=init安装任何内容的唯一答案-正是因为“导致它在实际安装任何东西之前就停止了”。实际安装可能已经发生,也可能没有发生。我要指出的是,您__version__ = dist.get_version()可能应该在主包中的某个地方使用它__init__.py。那对我有用。
bballdave025

1

根据接受的答案和评论,这就是我最终要做的事情:

文件: 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请求

  • 仅当实际安装了项目时,才会发生其他导入(用于简化的软件包界面)


1
FWIW,我不再以这种方式构造软件包,因为我不喜欢总是必须在中运行代码的想法__init__.py。我现在在安装过程中从包中“读”:github.com/jacebrowning/template-python-demo/blob/...
杰斯布朗宁

1

setuptools 46.4.0添加了基本的抽象语法树分析支持,因此setup.cfg attr:指令无需导入软件包的依赖项即可工作。这样就可以使软件包版本具有唯一的真实性,从而使在发布setupstools 46.4.0之前发布的先前答案中的许多解决方案变得过时。

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

没有将版本传递给安装程序的setup.py

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

-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轻松扩展以共享文档字符串等。谁能看到这种方法的问题?

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.