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

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

import foo
print(foo.version)

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

238658 次浏览

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

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

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

  2. 相反:将版本信息放在包本身中,并导入that来设置setup.py中的版本

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

这几乎是一个准标准。标准库中的许多模块使用__version__,这也用于第三方模块的很多中,因此它是准标准的。

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

正如S.Lott所提到的(谢谢!),PEP 8明确地说:

模块级别Dunder名称

模块级别"dunders"(即名称有两个开头和两个结尾 下划线),例如__all____author____version__等。 应该放在模块文档字符串之后,但在任何导入之前

. import

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

同样值得注意的是__version__是一个半std。在python中__version_info__也是,它是一个元组,在简单的情况下,你可以这样做:

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

...你可以从文件中获取__version__字符串,或者其他什么。

与其他一些答案相比,还有一个稍微简单一点的选择:

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

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

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

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


正如shezi提醒我的那样,(词汇)数字串的比较并不一定与直接的数字比较有相同的结果;需要前导零来提供这一点。因此,最后,将__version_info__(或任何它将被称为)存储为整数值的元组将允许更有效的版本比较。

我还看到了另一种风格:

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

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

  1. 它提供了一个__version__属性。

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

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

  4. 版本号只写在一个地方,所以当版本号发生变化时,只在一个地方更改它,并且版本不一致的可能性更小。

它是这样工作的:存储版本号的“一个规范位置”是一个.py文件,名为“_version.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将这个字符串作为"version"参数的值传递给setup(),这样就满足了特性2。

为了满足特性1,你可以让你的包(在运行时,而不是在设置时!)像这样从myniftyapp/__init__.py导入_version文件:

from _version import __version__

这是这是该技术的一个例子,我已经使用多年。

这个例子中的代码有点复杂,但是我写在这个注释中的简化的例子应该是一个完整的实现。

这里是导入版本示例代码

如果你发现这种方法有任何问题,请告诉我。

根据延迟的[STOP PRESS: 拒绝了] PEP 396(模块版本号),有一个建议的方法来做到这一点。它描述了模块遵循的基本原理(当然是可选的)标准。下面是一个片段:

  1. 当模块(或包)包含版本号时,版本号应该在__version__属性中可用。
  1. 对于位于命名空间包中的模块,该模块应该包含__version__属性。命名空间包本身不应该包含自己的__version__属性。
  1. __version__属性的值应该是一个字符串。

重写2017 - 05

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

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

到今天为止,pbr每月有1200万下载量,达到这个级别不包括任何肮脏的技巧。这只是一件事——用非常简单的方法解决一个常见的包装问题。

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

因此,为了让你了解在一次提交中采用pbr的情况,请查看将包装转换为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__,
...

如果你知道其他更好的方法,请告诉我。

我在包目录中使用了一个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中放入了其他信息,比如author。我 我喜欢使用JSON,因为我可以自动管理元数据

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

现在(since 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__'),
# [...]
)

如果您使用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)

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

  • git repo中的标记派生所有python版本引用
  • 用一个不需要输入的命令自动化git tag/pushsetup.py upload步骤。

工作原理:

  1. make release命令中,找到git repo中最后一个带标记的版本并对其进行递增。标记被推回到origin

  2. Makefile将版本存储在src/_version.py中,它将被setup.py读取并包含在发行版中。不要将_version.py检入源代码控制!< / >强

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

细节:

Makefile

# 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目标总是增加第三个版本的数字,但你可以使用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
  1. 只使用带有__version__ = <VERSION>参数的version.py文件。在setup.py文件中导入__version__参数,并将其值放入setup.py文件中,如下所示: 李version=__version__ < / >
  2. 另一种方法是只使用带有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:<RELATIVE_PATH_TO_VERSION_FILE>] < / >
  • 你在你的回购中必须 git commitgit reset所有东西,否则你会得到一个脏的回购错误。
  • 确保你的虚拟环境包含了bump版本的包,没有它它将无法工作。

使用setuptoolspbr

没有一个管理版本的标准方法,但是管理你的包的标准方法是setuptools

总的来说,我发现的管理版本的最佳解决方案是使用setuptoolspbr扩展名。这是我现在管理版本的标准方式。

对于简单的项目来说,为完整的打包设置项目可能有些过分,但是如果您需要管理版本,那么您可能处于设置所有内容的正确级别。这样做还可以使你的包在PyPi上发布,这样每个人都可以下载并与Pip一起使用它。

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

当在VCS/SCM中使用Git时,这种设置甚至更好,因为它将从Git中引入大量元数据,这样你的回购就可以成为一些元数据的主要真实来源,包括版本、作者、更改日志等。对于version,它将基于repo中的git标记为当前提交创建一个版本字符串。

由于PBR会直接从你的git repo中提取版本、作者、更新日志和其他信息,所以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回购信息更新到你的<mypackage>.egg-info元数据文件夹中最简单的方法是运行python setup.py install命令。这将运行所有与从git repo提取元数据和更新本地.egg-info文件夹相关的PBR函数,为您定义的任何入口点安装脚本可执行文件,以及运行此命令时可以从输出中看到的其他函数。

注意,.egg-info文件夹通常不会被存储在标准的Python .gitignore文件中(比如从Gitignore。IO),因为它可以从你的源代码中生成。如果它被排除在外,请确保您有一个标准的“发布流程”。以便在发布之前在本地更新元数据,并且您上传到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

经过几个小时的努力,我找到了最简单可靠的解决方案,以下是其中的几个部分:

在你的包"/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__,

在主文件夹__abc1 .py中:

from .version import __version__

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

大量关于统一版本和支持约定的工作已经完成自从这个问题第一次被提出。可接受的选项现在是Python打包用户指南中的详细的。同样值得注意的是版本号方案在Python PEP 440中是相对严格的,因此,如果你的包将被释放到奶酪店,保持事情正常是至关重要的。

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

  1. 读取setup.py (setuptools)中的文件并获得版本。
  2. 使用外部构建工具(更新__init__.py和源代码控制),例如bump2version变化zest.releaser
  3. 将值设置为特定模块中的__version__全局变量。
  4. 将值放在一个简单的VERSION文本文件中,以便阅读setup.py和代码。
  5. 通过setup.py版本设置该值,并在运行时使用importlib.metadata获取该值。(警告,有3.8之前和3.8之后的版本。)
  6. sample/__init__.py中将值设置为__version__,并在setup.py中导入sample。
  7. 使用setuptools_scm从源代码控制中提取版本控制,以便它是规范引用,而不是代码。

请注意(7)可能是最现代的方法(构建元数据独立于代码,由自动化发布)。还有请注意,如果setup用于包发布,则简单的python3 setup.py --version将直接报告版本。

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

from pkg_resources import get_distribution
                                                                                  

__version__ = get_distribution('foo').version

确保foo总是已经安装,这就是为什么需要一个src/层来防止在没有安装的情况下导入foo

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


2022.7.5中的更新:

还有另一种方法,这是我现在最喜欢的。使用setuptools-scm生成一个_version.py文件。

setup(
...
use_scm_version={
'write_to':
'src/foo/_version.py',
'write_to_template':
'"""Generated version file."""\n'
'__version__ = "{version}"\n',
},
)

带有bump2版本的策略路由

这个解决方案来自这篇文章

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

下面是项目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包含just

__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}-{release}{build}
{major}.{minor}.{patch}


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


[bumpversion:file:packagex/_version.py]
  1. 在与__init__.py相同的文件夹中创建一个名为_version.txt的文件,并将version写为一行:
0.8.2
  1. __init__.py中的文件_version.txt中读取以下信息:
    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()
我描述了一种标准和现代的方法,依赖于setuptools_scm。 在过去的几年里,这种模式已经成功地为几十个已发布的包工作,所以我可以热情地推荐它

注意,你不需要getversion包来实现这个模式。恰好getversion文档中有这个技巧。

使用setuptools和pyproject.toml

Setuptools现在提供了一种pyproject.toml中动态获取版本的方法

重现这里的例子,你可以在pyproject.toml中创建如下内容

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