将版本嵌入 Python 包的标准方法?

Posted

技术标签:

【中文标题】将版本嵌入 Python 包的标准方法?【英文标题】:Standard way to embed version into Python package? 【发布时间】:2010-10-02 07:03:33 【问题描述】:

是否有一种标准方法可以将版本字符串与 Python 包相关联,以便我可以执行以下操作?

import foo
print(foo.version)

我想有一些方法可以在没有任何额外硬编码的情况下检索该数据,因为在 setup.py 中已经指定了次要/主要字符串。我发现的替代解决方案是在我的foo/__init__.py 中包含import __version__,然后由setup.py 生成__version__.py

【问题讨论】:

仅供参考,有一个很好的概述:packaging.python.org/en/latest/… 可以使用 setuptools 从元数据中检索已安装软件包的版本,因此在许多情况下,仅将版本放在 setup.py 中就足够了。见this question。 仅供参考,基本上有 5 common patterns 来维护版本号的单一事实来源(在设置和运行时)。 @ionelmc 您的链接已损坏。这是更新的packaging.python.org/guides/single-sourcing-package-version 这应该更简单。看看这个简单问题的所有复杂答案。哎呀。如果setup.py 有一个版本(它必须),为什么package_name.__version__ 不能正常工作并显示该版本? 【参考方案1】:

对于它的价值,如果你使用 NumPy distutils,numpy.distutils.misc_util.Configuration 有一个 make_svn_version_py() 方法,它将修订号嵌入到变量 package.__svn_version__ 中的变量 version 中。

【讨论】:

您能否提供更多详细信息或示例说明其工作原理? 嗯。在 2020 年,这(是否一直如此?)是为 FORTRAN 设计的。包“numpy.distutils 是 NumPy 扩展标准 Python distutils 以处理 Fortran 源代码的一部分。”【参考方案2】:

似乎没有将版本字符串嵌入 python 包的标准方法。我见过的大多数软件包都使用您的解决方案的某些变体,即 eitner

    将版本嵌入setup.py 并让setup.py 生成一个仅包含版本信息的模块(例如version.py),该模块由您的包导入,或者

    反之:将版本信息放入你的包本身,并导入 that 以在setup.py 中设置版本

【讨论】:

我喜欢你的建议,但是如何从 setup.py 生成这个模块? 我喜欢选项(1)的想法,它似乎比 Zooko 从文件中解析版本号的答案更简单。但是当开发人员克隆您的存储库时,您无法确保创建 version.py。除非您签入 version.py,否则您可能会更改 setup.py 中的版本号,提交、发布,然后必须(斜线忘记)提交对 version.py 的更改。 大概你可以使用类似 """ with open("mypackage/version.py", "w") as fp: fp.write("__version__ == '%s' \n" % (__version__,))""" 我认为选项 2. 在安装过程中容易失败,如 cmets 对 JAB 的回答所述。 那怎么样?您将 __version__ = '0.0.1'" (当然版本是字符串)放在软件主包的 __init__.py" 中。然后转到第 2 点:在从 package.__init__ import __version__ as v 执行的设置中,然后将变量 v 设置为 setup.py 的版本。然后 import mypack as my, print my.__version__ 将打印版本。版本将仅存储在一个文件中,可从整个代码访问,该文件不会导入与软件相关的任何其他内容。【参考方案3】:

不能直接回答您的问题,但您应该考虑将其命名为 __version__,而不是 version

这几乎是一个准标准。标准库中很多模块使用__version__,第三方模块的lots也使用了lots,所以是准标准的。

通常,__version__ 是一个字符串,但有时它也是一个浮点数或元组。

编辑:正如 S.Lott 所述(谢谢!),PEP 8 明确表示:

模块级 Dunder 名称

模块级别的“dunders”(即具有两个前导和两个尾随的名称 下划线)如__all____author____version__等。 应该放在模块文档字符串之后但在任何导入之前 来自 __future__ 导入之外的语句。

您还应确保版本号符合PEP 440 中描述的格式(PEP 386 此标准的先前版本)。

【讨论】:

它应该是一个字符串,并且有一个 version_info 表示元组版本。 James:为什么是__version_info__? (它“发明”了你自己的双下划线词。)[当詹姆斯评论时,下划线在 cmets 中没有任何作用,现在它们表示强调,所以詹姆斯真的也写了__version_info__。 ---编辑] 您可以在packages.python.org/distribute/… 看到version 应该说的内容,该页面是关于分发的,但版本号的含义正在成为事实上的标准。 不管 PEP 8 怎么说,__version__ 的内容最好遵循 PEP 386 中的规则,以便可靠地比较版本号。 你会把那个版本放在哪里。考虑到这是已接受的版本,我很想在这里看到更多信息。【参考方案4】:

另外值得注意的是__version__ 是一个半标准。在 python 中,__version_info__ 也是一个元组,在简单的情况下,您可以执行以下操作:

__version__ = '1.2.3'
__version_info__ = tuple([ int(num) for num in __version__.split('.')])

...您可以从文件中获取__version__ 字符串,或其他任何内容。

【讨论】:

你有关于__version_info__这种用法的起源的任何参考/链接吗? 嗯,它与 sys.version 到 sys.version_info 的映射相同。所以:docs.python.org/library/sys.html#sys.version_info 在另一个方向(__version_info__ = (1, 2, 3)__version__ = '.'.join(map(str, __version_info__)))进行映射更容易。 @EOL - __version__ = '.'.join(str(i) for i in __version_info__) - 稍长但更 Pythonic。 我不确定你的提议显然更 Pythonic,因为它包含一个实际上并不需要且其含义有点难以表达的虚拟变量(i 没有意义,@ 987654331@ 有点长而且模棱两可……)。我什至认为 Python 中 map() 的存在强烈暗示它应该在这里使用,因为我们需要在这里做的是 map() 的典型用例(与现有函数一起使用)——我不查看许多其他合理的用途。【参考方案5】:

虽然这可能为时已晚,但有一个比上一个答案更简单的替代方案:

__version_info__ = ('1', '2', '3')
__version__ = '.'.join(__version_info__)

(使用str() 将版本号的自动递增部分转换为字符串相当简单。)

当然,据我所见,人们在使用__version_info__ 时倾向于使用与前面提到的版本类似的东西,因此将其存储为整数元组;但是,我不太明白这样做的意义,因为我怀疑在某些情况下您会出于好奇或自动递增之外的任何目的执行数学运算,例如对部分版本号进行加减运算(即使那样, int()str() 可以很容易地使用)。 (另一方面,其他人的代码可能期望数字元组而不是字符串元组,因此可能会失败。)

当然,这是我自己的观点,我很乐意听取其他人关于使用数字元组的意见。


正如shezi提醒我的那样,数字字符串的(词法)比较不一定与直接数字比较具有相同的结果;为此需要前导零。所以最后,将__version_info__(或其他任何名称)存储为整数值的元组将允许更有效的版本比较。

【讨论】:

不错 (+1),但您不是更喜欢数字而不是字符串吗?例如__version_info__ = (1,2,3) 当版本号超过 9 时,字符串比较会变得很危险,例如 '10' 我也这样做了,但稍作调整以解决整数.. __version_info__ = (0, 1, 0) __version__ = '.'.join(map(str, __version_info__)) __version__ = '.'.join(__version_info__) 的问题是__version_info__ = ('1', '2', 'beta') 将变为1.2.beta,而不是1.2beta1.2 beta 我认为这种方法的问题在于将声明__version__的代码行放在哪里。如果它们在 setup.py 中,那么您的程序无法从其包中导入它们。也许这对你来说不是问题,在这种情况下,很好。如果它们在您的程序中,那么您的 setup.py 可以导入它们,但它不应该,因为 setup.py 在安装期间运行,而您的程序的依赖项尚未安装(setup.py 用于确定依赖项是什么.) 因此 Zooko 的回答是:手动从产品源文件中解析值,而不导入产品包【参考方案6】:

我还看到了另一种风格:

>>> django.VERSION
(1, 1, 0, 'final', 0)

【讨论】:

是的,我也看到了。顺便说一句,每个答案都采用其他风格,所以现在我不知道什么风格是“标准”。正在寻找提到的 PEP... 另一种方式; Mongo 的 Python 客户端使用纯版本,没有下划线。所以这行得通; $ python >>> import pymongo >>> pymongo.version '2.7' 实现.VERSION并不意味着你不必实现__version__ 这需要在项目中实现django吗?【参考方案7】:

我使用单个_version.py 文件作为“曾经的规范位置”来存储版本信息:

    它提供了一个__version__属性。

    提供标准元数据版本。因此,pkg_resources 或其他解析包元数据的工具(EGG-INFO 和/或 PKG-INFO,PEP 0345)会检测到它。

    在构建你的包时它不会导入你的包(或其他任何东西),这在某些情况下可能会导致问题。 (请参阅下面的 cmets,了解这会导致什么问题。)

    版本号写的地方只有一个,所以版本号变化的时候也只有一个地方可以改,版本不一致的可能性更小。

它是这样工作的:存储版本号的“一个规范位置”是一个名为“_version.py”的 .py 文件,它位于您的 Python 包中,例如在myniftyapp/_version.py 中。这个文件是一个 Python 模块,但是你的 setup.py 没有导入它! (这会破坏功能 3。)相反,您的 setup.py 知道该文件的内容非常简单,例如:

__version__ = "3.6.5"

所以你的 setup.py 打开文件并解析它,代码如下:

import re
VERSIONFILE="myniftyapp/_version.py"
verstrline = open(VERSIONFILE, "rt").read()
VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
mo = re.search(VSRE, verstrline, re.M)
if mo:
    verstr = mo.group(1)
else:
    raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,))

然后您的 setup.py 将该字符串作为“版本”参数的值传递给setup(),从而满足功能 2。

为了满足功能 1,您可以让您的包(在运行时,而不是在设置时!)从 myniftyapp/__init__.py 导入 _version 文件,如下所示:

from _version import __version__

这是我多年来一直使用的an example of this technique。

该示例中的代码有点复杂,但我在此评论中写的简化示例应该是一个完整的实现。

这里是example code of importing the version。

如果您发现此方法有任何问题,请告诉我。

【讨论】:

您能描述一下激发#3 的问题吗? Glyph 说这与“当你的 setup.py 运行时,setuptools 喜欢假装你的代码不在系统上的任何位置”有关,但细节将有助于说服我和其他人。 @Iva 现在,该工具应该按什么顺序执行此操作?在评估您的setup.py 之前,它甚至无法(在今天的 setuptools/pip/virtualenv 系统中)知道deps 是什么。此外,如果它尝试完全深度优先并在执行此操作之前先完成所有 deps,如果存在循环 deps,它会卡住。但是如果它在安装依赖项之前尝试构建这个包,那么如果你从你的setup.py 导入你的包,它不一定能够导入它的 deps,或者它的 deps 的正确版本。 你能从“setup.py”写入文件“version.py”而不是解析它吗?这似乎更简单。 Jonathan Hartley:我同意您的“setup.py”编写“version.py”文件而不是解析它会稍微简单一些,但它会打开一个不一致的窗口,当您已编辑 setup.py 以获得新版本,但尚未执行 setup.py 来更新 version.py 文件。将规范版本放在一个小的单独文件中的另一个原因是,它使 其他 工具(例如读取您的修订控制状态的工具)可以轻松编写版本文件。 类似的方法是从 setup.py 中 execfile("myniftyapp/_version.py"),而不是尝试手动解析版本代码。在***.com/a/2073599/647002 中提出建议——讨论也可能有帮助。【参考方案8】:

根据延迟的PEP 396 (Module Version Numbers),有一个建议的方法来做到这一点。它以基本原理描述了模块要遵循的(当然是可选的)标准。这是一个sn-p:

3) 当模块(或包)包含版本号时,该版本应该在__version__ 属性中可用。

4) 对于存在于命名空间包中的模块,模块应该包含__version__ 属性。命名空间包本身不应包含其自己的__version__ 属性。

5) __version__ 属性的值应该是一个字符串。

【讨论】:

PEP 不被接受/标准化,但被推迟(由于缺乏兴趣)。因此,声明它指定了“有一种标准方式”有点误导。 @weaver:天哪!我学到了一些新东西。我不知道这是我需要检查的东西。 编辑注意它不是一个标准。现在我感到很尴尬,因为我对项目提出了功能要求,要求他们遵循这个“标准”。 也许你应该接管那个 PEP 的标准化工作,因为你似乎很感兴趣 :) 这适用于对单个模块进行版本控制,但我不确定它是否适用于对完整项目进行版本控制。【参考方案9】:

重写 2017-05

在编写 Python 代码和管理各种包超过 13 年之后,我得出的结论是,DIY 可能不是最好的方法。

我开始使用pbr 包来处理我的包中的版本控制。如果您使用 git 作为 SCM,这将像魔术一样融入您的工作流程,节省您数周的工作时间(您会惊讶于问题的复杂程度)。

截至今天,pbr has 12M mongthly downloads,达到这个水平并没有包含任何肮脏的伎俩。这只是一件事——以一种非常简单的方式解决常见的包装问题。

pbr 可以承担更多的包维护负担,并且不限于版本控制,但它不会强迫您采用它的所有好处。

为了让您了解在一次提交中采用 pbr 的情况,请查看switching packaging to pbr

您可能会观察到版本根本没有存储在存储库中。 PBR 确实从 Git 分支和标签中检测到它。

无需担心当您没有 git 存储库时会发生什么,因为 pbr 会在您打包或安装应用程序时“编译”并缓存版本,因此不存在对 git 的运行时依赖。

旧解决方案

这是迄今为止我见过的最好的解决方案,它也解释了原因:

内部yourpackage/version.py

# Store the version here so:
# 1) we don't load dependencies by storing it in __init__.py
# 2) we can import it in setup.py for the same reason
# 3) we can import it into your module module
__version__ = '0.12'

内部yourpackage/__init__.py

from .version import __version__

内部setup.py

exec(open('yourpackage/version.py').read())
setup(
    ...
    version=__version__,
    ...

如果您知道另一种似乎更好的方法,请告诉我。

【讨论】:

错误,没有。 Python 3 中不存在 execfile(),所以最好使用 exec(open().read())。 为什么不在 setup.py 中也有from .version import __version__ @Aprillion 因为在setup.py 运行时没有加载包 - 试试吧,你会得到一个错误(或者至少,我做到了:-)) 指向 pbr 的链接导致网关错误。 pbr 无疑是一个很棒的工具,但您没有解决这个问题。如何通过 bpr 访问当前版本或已安装的包。【参考方案10】:

我在包目录中使用 JSON 文件。这符合 Zooko 的要求。

内部pkg_dir/pkg_info.json

"version": "0.1.0"

内部setup.py

from distutils.core import setup
import json

with open('pkg_dir/pkg_info.json') as fp:
    _info = json.load(fp)

setup(
    version=_info['version'],
    ...
    )

内部pkg_dir/__init__.py

import json
from os.path import dirname

with open(dirname(__file__) + '/pkg_info.json') as fp:
    _info = json.load(fp)

__version__ = _info['version']

我还把其他信息放在pkg_info.json,比如作者。一世 喜欢使用 JSON,因为我可以自动管理元数据。

【讨论】:

您能否详细说明如何使用 json 来自动化元数据管理?谢谢!【参考方案11】:

arrow 以一种有趣的方式处理它。

现在(自2e5031b)

arrow/__init__.py:

__version__ = 'x.y.z'

setup.py:

from arrow import __version__

setup(
    name='arrow',
    version=__version__,
    # [...]
)

之前

arrow/__init__.py:

__version__ = 'x.y.z'
VERSION = __version__

setup.py:

def grep(attrname):
    pattern = r"0\W*=\W*'([^']+)'".format(attrname)
    strval, = re.findall(pattern, file_text)
    return strval

file_text = read(fpath('arrow/__init__.py'))

setup(
    name='arrow',
    version=grep('__version__'),
    # [...]
)

【讨论】:

什么是file_text 更新后的解决方案实际上是有害的。当 setup.py 运行时,它不一定会从本地文件路径中看到包的版本。它可能会恢复到以前安装的版本,例如从在开发分支上运行pip install -e . 或在测试时运行。 setup.py 绝对不应该依赖于导入它在安装过程中的包来确定部署参数。哎呀。 是的,我不知道为什么箭头决定回归到这个解决方案。此外,提交消息显示“使用 现代 Python 标准更新 setup.py”...?【参考方案12】:

如果您使用 CVS(或 RCS)并想要快速解决方案,您可以使用:

__version__ = "$Revision: 1.1 $"[11:-2]
__version_info__ = tuple([int(s) for s in __version__.split(".")])

(当然,修订号会被CVS代替。)

这为您提供了一个易于打印的版本和一个版本信息,您可以使用它来检查您正在导入的模块是否至少具有预期的版本:

import my_module
assert my_module.__version_info__ >= (1, 1)

【讨论】:

您建议将__version__ 保存到哪个文件?使用此解决方案如何增加版本号?【参考方案13】:

这里的许多解决方案都忽略了git 版本标签,这仍然意味着您必须在多个地方跟踪版本(不好)。我的目标是:

git repo 中的标签派生所有 python 版本引用 使用无需输入的单个命令自动执行 git tag/pushsetup.py upload 步骤。

工作原理:

    通过make release 命令,在 git 存储库中找到并增加最后一个标记版本。标签被推回origin

    Makefile 将版本存储在src/_version.py 中,setup.py 将读取该版本并包含在发行版中。 不要将_version.py 签入源代码管理!

    setup.py 命令从package.__version__ 读取新版本字符串。

详情:

生成文件

# remove optional 'v' and trailing hash "v1.0-N-HASH" -> "v1.0-N"
git_describe_ver = $(shell git describe --tags | sed -E -e 's/^v//' -e 's/(.*)-.*/\1/')
git_tag_ver      = $(shell git describe --abbrev=0)
next_patch_ver = $(shell python versionbump.py --patch $(call git_tag_ver))
next_minor_ver = $(shell python versionbump.py --minor $(call git_tag_ver))
next_major_ver = $(shell python versionbump.py --major $(call git_tag_ver))

.PHONY: $MODULE/_version.py
$MODULE/_version.py:
    echo '__version__ = "$(call git_describe_ver)"' > $@

.PHONY: release
release: test lint mypy
    git tag -a $(call next_patch_ver)
    $(MAKE) $MODULE/_version.py
    python setup.py check sdist upload # (legacy "upload" method)
    # twine upload dist/*  (preferred method)
    git push origin master --tags

release 目标始终递增第 3 个版本数字,但您可以使用 next_minor_vernext_major_ver 递增其他数字。这些命令依赖于检查到 repo 根目录的 versionbump.py 脚本

versionbump.py

"""An auto-increment tool for version strings."""

import sys
import unittest

import click
from click.testing import CliRunner  # type: ignore

__version__ = '0.1'

MIN_DIGITS = 2
MAX_DIGITS = 3


@click.command()
@click.argument('version')
@click.option('--major', 'bump_idx', flag_value=0, help='Increment major number.')
@click.option('--minor', 'bump_idx', flag_value=1, help='Increment minor number.')
@click.option('--patch', 'bump_idx', flag_value=2, default=True, help='Increment patch number.')
def cli(version: str, bump_idx: int) -> None:
    """Bumps a MAJOR.MINOR.PATCH version string at the specified index location or 'patch' digit. An
    optional 'v' prefix is allowed and will be included in the output if found."""
    prefix = version[0] if version[0].isalpha() else ''
    digits = version.lower().lstrip('v').split('.')

    if len(digits) > MAX_DIGITS:
        click.secho('ERROR: Too many digits', fg='red', err=True)
        sys.exit(1)

    digits = (digits + ['0'] * MAX_DIGITS)[:MAX_DIGITS]  # Extend total digits to max.
    digits[bump_idx] = str(int(digits[bump_idx]) + 1)  # Increment the desired digit.

    # Zero rightmost digits after bump position.
    for i in range(bump_idx + 1, MAX_DIGITS):
        digits[i] = '0'
    digits = digits[:max(MIN_DIGITS, bump_idx + 1)]  # Trim rightmost digits.
    click.echo(prefix + '.'.join(digits), nl=False)


if __name__ == '__main__':
    cli()  # pylint: disable=no-value-for-parameter

这完成了如何从git 处理和增加版本号的繁重工作。

__init__.py

my_module/_version.py 文件被导入到my_module/__init__.py。将您希望与模块一起分发的任何静态安装配置放在此处。

from ._version import __version__
__author__ = ''
__email__ = ''

setup.py

最后一步是从my_module 模块中读取版本信息。

from setuptools import setup, find_packages

pkg_vars  = 

with open("MODULE/_version.py") as fp:
    exec(fp.read(), pkg_vars)

setup(
    version=pkg_vars['__version__'],
    ...
    ...
)

当然,要使所有这些工作,你必须在你的仓库中至少有一个版本标签才能开始。

git tag -a v0.0.1

【讨论】:

确实 - 整个线程忘记了 VCS 在本次讨论中非常重要。只是一个 obs:版本增加应该仍然是一个手动过程,所以首选的方式是 1. 手动创建并推送标签 2. 让 VCS 工具发现该标签并将其存储在需要的地方(哇 - 这个 SO 编辑界面真的让我很崩溃- 为了处理换行符,我不得不编辑了十几次,但它仍然不起作用!@#$%^&*) 当我们有一个很棒的python包bumpversion时,不需要使用versionbump.py @Oran 我查看了 versionbump。文档不是很清楚,也不能很好地处理标记。在我的测试中,它似乎进入了导致它崩溃的状态: subprocess.CalledProcessError: Command '['git', 'commit', '-F', '/var/folders/rl/tjyk4hns7kndnx035p26wg692g_7t8/T/tmppishngbo'] ' 返回非零退出状态 1. 为什么不应该使用版本控制来跟踪_version.py @StevenVascellaro 这使发布过程复杂化。现在您必须确保您的标签和提交与 _version.py 中的值匹配。使用单个版本标签更清洁 IMO,这意味着您可以直接从 github UI 创建一个版本。【参考方案14】:
    仅使用version.py 文件和文件中的__version__ = <VERSION> 参数。在setup.py 文件中导入__version__ 参数并将其值放入setup.py 文件中,如下所示: version=__version__ 另一种方法是仅使用带有 version=<CURRENT_VERSION>setup.py 文件 - CURRENT_VERSION 是硬编码的。

由于我们不想每次创建新标签(准备发布新的包版本)时都手动更改文件中的版本,我们可以使用以下..

我强烈推荐bumpversion 包。多年来我一直在使用它来提升版本。

如果您还没有 version=<VERSION> 文件,请先将其添加到您的 setup.py 文件中。

每次更新版本时都应该使用这样的短脚本:

bumpversion (patch|minor|major) - choose only one option
git push
git push --tags

然后为每个 repo 添加一个文件,名为:.bumpversion.cfg:

[bumpversion]
current_version = <CURRENT_TAG>
commit = True
tag = True
tag_name = new_version
[bumpversion:file:<RELATIVE_PATH_TO_SETUP_FILE>]

注意:

您可以像其他帖子中建议的那样使用version.py 文件下的__version__ 参数,并像这样更新bumpversion 文件: [bumpversion:file:&lt;RELATIVE_PATH_TO_VERSION_FILE&gt;]必须 git commitgit reset 回购中的所有内容,否则您将收到脏回购错误。 确保你的虚拟环境中包含了bumpversion的包,没有它就无法工作。

【讨论】:

@cmcginty 抱歉耽搁了,请检查我的回答 ^^^ - 请注意,您必须 git commit 或 git reset 仓库中的所有内容,并确保您的虚拟环境包含 bumpversion 的包,没有它就行不通。使用最新版本。 我有点不清楚这里建议的是什么解决方案。您推荐使用version.py手动跟踪版本,还是使用bumpversion跟踪它? @StevenVascellaro 我建议使用bumpversion,永远不要使用手动版本控制。我试图解释的是,您可以直接使用 bumpversion 来更新 setup.py 文件中的版本,或者更好地使用它来更新 version.py 文件。更常见的做法是更新 version.py 文件并将 __version__ 参数值放入 setup.py 文件中。我的解决方案用于生产,这是一种常见的做法。注意:要明确一点,使用bumpversion作为脚本的一部分是最好的解决方案,把它放在你的CI中,它会自动运行。【参考方案15】:

使用setuptoolspbr

没有标准的版本管理方式,但管理包的标准方式是setuptools

我发现总体上管理版本的最佳解决方案是使用带有pbr 扩展名的setuptools。这是我现在管理版本的标准方式。

为简单的项目设置完整打包的项目可能有点过头了,但如果您需要管理版本,您可能处于正确的级别来设置所有内容。这样做还可以使您的软件包在 PyPi 上发布,因此每个人都可以下载并使用 Pip。

PBR 将大多数元数据从 setup.py 工具中移出并移至 setup.cfg 文件中,然后该文件用作大多数元数据的来源,其中可能包括版本。这允许在需要时使用pyinstaller 之类的东西将元数据打包到可执行文件中(如果需要,您可能会使用need this info),并将元数据与其他包管理/设置脚本分开。您可以直接手动更新setup.cfg 中的版本字符串,在构建您的包发布时,它将被拉入*.egg-info 文件夹。然后,您的脚本可以使用各种方法从元数据中访问版本(这些过程在下面的部分中进行了概述)。

将 Git 用于 VCS/SCM 时,此设置会更好,因为它会从 Git 中提取大量元数据,这样您的存储库就可以成为某些元数据(包括版本、作者)的主要真实来源, changelogs 等。对于版本,它将根据 repo 中的 git 标签为当前提交创建一个版本字符串。

PyPA - Packaging Python Packages with SetupTools - Tutorial PBR latest build usage documentation - 如何设置包含元数据的 8 行 setup.pysetup.cfg 文件。

由于 PBR 将直接从您的 git 存储库中提取版本、作者、更改日志和其他信息,因此可以忽略 setup.cfg 中的一些元数据并在为您的包创建分发时自动生成(使用 setup.py )


实时获取当前版本

setuptools 将使用setup.py 实时获取最新信息:

python setup.py --version

这将从setup.cfg 文件或git repo 中提取最新版本,基于所做的最新提交和repo 中存在的标签。不过,此命令不会更新发行版中的版本。


更新版本元数据

当您使用setup.py(例如py setup.py sdist)创建分发时,所有当前信息将被提取并存储在分发中。这实质上是运行setup.py --version 命令,然后将该版本信息存储到package.egg-info 文件夹中的一组文件中,这些文件存储分发元数据。

版本元数据更新流程说明:

如果您不使用 pbr 从 git 中提取版本数据,则只需使用新版本信息直接更新您的 setup.cfg(很简单,但请确保这是发布过程的标准部分)。

如果您使用 git,并且不需要创建源代码或二进制分发版(使用 python setup.py sdistpython setup.py bdist_xxx 命令之一),最简单的方法是将 git repo 信息更新到您的 &lt;mypackage&gt;.egg-info元数据文件夹只是运行python setup.py install 命令。这将运行与从 git 存储库中提取元数据相关的所有 PBR 函数,并更新您的本地 .egg-info 文件夹,为您定义的任何入口点安装脚本可执行文件,以及您在运行此命令时可以从输出中看到的其他函数.

请注意,.egg-info 文件夹通常被排除在标准 Python .gitignore 文件(例如来自Gitignore.IO)中的 git 存储库本身中,因为它可以从您的源代码生成。如果它被排除在外,请确保您有一个标准的“发布流程”来在发布前在本地更新元数据,并且您上传到 PyPi.org 或以其他方式分发的任何包都必须包含此数据才能获得正确的版本。如果您希望 Git 存储库包含此信息,您可以排除特定文件被忽略(即添加 !*.egg-info/PKG_INFO.gitignore


从脚本访问版本

您可以在包本身的 Python 脚本中访问当前构建的元数据。例如,对于版本,到目前为止我发现有几种方法可以做到这一点:

## This one is a new built-in as of Python 3.8.0 should become the standard
from importlib.metadata import version

v0 = version("mypackage")
print('v0 '.format(v0))

## I don't like this one because the version method is hidden
import pkg_resources  # part of setuptools

v1 = pkg_resources.require("mypackage")[0].version
print('v1 '.format(v1))

# Probably best for pre v3.8.0 - the output without .version is just a longer string with
# both the package name, a space, and the version string
import pkg_resources  # part of setuptools

v2 = pkg_resources.get_distribution('mypackage').version
print('v2 '.format(v2))

## This one seems to be slower, and with pyinstaller makes the exe a lot bigger
from pbr.version import VersionInfo

v3 = VersionInfo('mypackage').release_string()
print('v3 '.format(v3))

您可以将其中一个直接放入您的 __init__.py 以获取包中的版本信息,如下所示,类似于其他一些答案:

__all__ = (
    '__version__',
    'my_package_name'
)

import pkg_resources  # part of setuptools

__version__ = pkg_resources.get_distribution("mypackage").version

【讨论】:

【参考方案16】:

经过几个小时试图找到最简单可靠的解决方案,以下是部分内容:

在你的包“/mypackage”文件夹内创建一个version.py文件:

# Store the version here so:
# 1) we don't load dependencies by storing it in __init__.py
# 2) we can import it in setup.py for the same reason
# 3) we can import it into your module module
__version__ = '1.2.7'

在 setup.py 中:

exec(open('mypackage/version.py').read())
setup(
    name='mypackage',
    version=__version__,

在主文件夹 init.py:

from .version import __version__

exec() 函数在任何导入之外运行脚本,因为 setup.py 在导入模块之前运行。您仍然只需要在一个地方管理一个文件中的版本号,但不幸的是它不在 setup.py 中。 (这是缺点,但没有导入错误是优点)

【讨论】:

【参考方案17】:

自从第一次提出这个问题以来,已经完成了大量关于统一版本控制和支持约定的工作。现在,Python Packaging User Guide 中详细说明了可口选项。同样值得注意的是version number schemes are relatively strict in Python per PEP 440,因此如果您的软件包将被发布到Cheese Shop,那么保持理智至关重要。

以下是版本控制选项的简短细分:

    读取setup.py(setuptools)中的文件并获取版本。 使用外部构建工具(更新__init__.py 以及源代码控制),例如bump2version、changes 或 zest.releaser。 将值设置为特定模块中的__version__ 全局变量。 将值放在一个简单的 VERSION 文本文件中,供 setup.py 和代码读取。 通过setup.py 版本设置值,并使用importlib.metadata 在运行时获取它。 (警告,有 3.8 之前和 3.8 之后的版本。) 将sample/__init__.py中的值设置为__version__,并在setup.py中导入样本。 使用setuptools_scm 从源代码管理中提取版本控制,使其成为规范参考,而不是代码。

注意 (7) 可能是most modern approach(构建元数据独立于代码,由自动化发布)。另外注意,如果安装程序用于软件包发布,简单的python3 setup.py --version 将直接报告版本。

【讨论】:

【参考方案18】:

我更喜欢从安装环境中读取软件包版本。 这是我的src/foo/_version.py

from pkg_resources import get_distribution                                        
                                                                                  
__version__ = get_distribution('osbc').version

确保foo 始终已安装,这就是为什么需要src/ 层来防止在未安装的情况下导入foo

setup.py中,我使用setuptools-scm自动生成版本。

【讨论】:

【参考方案19】:

带有bump2version的pbr

此解决方案源自this article

用例 - 通过 PyInstaller 分发的 python GUI 包。需要显示版本信息。

这里是项目结构packagex

packagex
├── packagex
│   ├── __init__.py
│   ├── main.py
│   └── _version.py
├── packagex.spec
├── LICENSE
├── README.md
├── .bumpversion.cfg
├── requirements.txt
├── setup.cfg
└── setup.py

setup.py 在哪里

# setup.py
import os

import setuptools

about = 
with open("packagex/_version.py") as f:
    exec(f.read(), about)

os.environ["PBR_VERSION"] = about["__version__"]

setuptools.setup(
    setup_requires=["pbr"],
    pbr=True,
    version=about["__version__"],
)

packagex/_version.py 只包含

__version__ = "0.0.1"

packagex/__init__.py

from ._version import __version__

对于.bumpversion.cfg

[bumpversion]
current_version = 0.0.1
commit = False
tag = False
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+)(?P<build>\d+))?
serialize = 
    major.minor.patch-releasebuild
    major.minor.patch

[bumpversion:part:release]
optional_value = prod
first_value = dev
values = 
    dev
    prod

[bumpversion:file:packagex/_version.py]

【讨论】:

【参考方案20】:
    在与__init__.py 相同的文件夹中创建一个以_version.txt 命名的文件,并将版本写为一行:
0.8.2
    从文件_version.txt 中的__init__.py 中读取此信息:
    import os 
    def get_version():
        with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), "_version.txt")) as f:
            return f.read().strip() 
    __version__ = get_version()

【讨论】:

以上是关于将版本嵌入 Python 包的标准方法?的主要内容,如果未能解决你的问题,请参考以下文章

python嵌入式版本安装openpyxl失败解决方法

R语言可视化密度图并在密度图中嵌入图例信息使用geomtextpath包的geom_textdensity函数,将图例(legend)信息嵌入到密度图中

我的嵌入式 python 标准输出在哪里?

在没有标准库的情况下嵌入 Python3

嵌入 Python 时共享数组的最简单方法

将句子转换为嵌入表示