是否有一种标准方法可以将版本字符串与 Python 包相关联,以便我可以执行以下操作?
import foo
print(foo.version)
我想有一些方法可以在没有任何额外硬编码的情况下检索该数据,因为setup.py
已经指定了次要/主要字符串。我发现的替代解决方案是import __version__
在我的foo/__init__.py
,然后__version__.py
由setup.py
.
不是直接回答您的问题,但您应该考虑将其命名为__version__
,而不是version
.
这几乎是一个准标准。标准库中的很多模块都用到了,很多第三方模块__version__
也用到了这个,所以是准标准的。
通常,__version__
是一个字符串,但有时它也是一个浮点数或元组。
编辑:正如 S.Lott 所述(谢谢!),PEP 8明确表示:
模块级 Dunder 名称
模块级别的“dunders”(即带有两个前导和两个下划线的名称),例如,,,等
__all__
应该放在模块文档字符串之后,但在任何导入语句之前,除了来自导入。__author__
__version__
__future__
我使用单个_version.py
文件作为“曾经规范的地方”来存储版本信息:
它提供了一个__version__
属性。
它提供标准的元数据版本。因此,它将被pkg_resources
解析包元数据的其他工具(EGG-INFO 和/或 PKG-INFO,PEP 0345)检测到。
在构建你的包时它不会导入你的包(或其他任何东西),这在某些情况下可能会导致问题。(有关这可能导致的问题,请参阅下面的评论。)
版本号写下来的地方只有一个,所以版本号变化的时候也只有一个地方可以改,版本不一致的几率也比较小。
它是这样工作的:存储版本号的“一个规范位置”是一个 .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 将该字符串作为“版本”参数的值传递给setup()
,从而满足功能 2。
为了满足功能 1,您可以让您的包(在运行时,而不是在设置时!)导入 _version 文件,myniftyapp/__init__.py
如下所示:
from _version import __version__
这是我多年来一直使用的这种技术的一个例子。
该示例中的代码稍微复杂一些,但我在此注释中编写的简化示例应该是一个完整的实现。
这是导入版本的示例代码。
如果您发现这种方法有任何问题,请告诉我。
在编写 Python 代码和管理各种包 13 年多之后,我得出的结论是 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__,
...
如果您知道另一种似乎更好的方法,请告诉我。
根据延迟的PEP 396(模块版本号),有一个建议的方法来做到这一点。它以基本原理描述了模块要遵循的(当然是可选的)标准。这是一个片段:
3) 当模块(或包)包含版本号时,该版本应该在
__version__
属性中可用。4) 对于存在于命名空间包中的模块,模块应该包含该
__version__
属性。命名空间包本身不应该包含它自己的__version__
属性。5)
__version__
属性的值应该是一个字符串。
尽管这可能为时已晚,但有一个比先前答案更简单的替代方案:
__version_info__ = ('1', '2', '3')
__version__ = '.'.join(__version_info__)
(使用 . 将版本号的自动递增部分转换为字符串相当简单str()
。)
当然,据我所见,人们在使用 时倾向于使用前面提到的版本__version_info__
,因此将其存储为整数元组;但是,我不太明白这样做的意义,因为我怀疑在某些情况下您会出于好奇或自动递增之外的任何目的对部分版本号执行数学运算,例如加法和减法(即使那样,int()
并且str()
可以相当容易地使用)。(另一方面,其他人的代码可能期望数字元组而不是字符串元组,从而失败。)
当然,这是我自己的观点,我很乐意听取其他人关于使用数字元组的意见。
正如shezi提醒我的那样,数字字符串的(词法)比较不一定与直接数字比较具有相同的结果;为此需要前导零。所以最后,将__version_info__
(或任何它被称为的)存储为整数值的元组将允许更有效的版本比较。
这里的许多解决方案都忽略了git
版本标签,这仍然意味着您必须在多个地方跟踪版本(不好)。我的目标是:
git
从repo中的标记派生所有 python 版本引用git tag
/push
和步骤。setup.py upload
通过make release
命令,可以找到 git repo 中的最后一个标记版本并递增。标签被推回到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 个版本的数字,但您可以使用ornext_minor_ver
来next_major_ver
增加其他数字。这些命令依赖于versionbump.py
检查到 repo 根目录的脚本
"""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
。
文件被my_module/_version.py
导入到my_module/__init__.py
. 将您希望与模块一起分发的任何静态安装配置放在这里。
from ._version import __version__
__author__ = ''
__email__ = ''
最后一步是从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
我在包目录中使用 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,因为我可以自动管理元数据。
自从第一次提出这个问题以来,已经完成了大量针对统一版本控制和支持约定的工作。Python Packaging User Guide中详细介绍了可口的选项。另外值得注意的是,Python 中的版本号方案相对严格,每个 PEP 440,因此如果您的包将发布到Cheese Shop ,那么保持一切正常至关重要。
以下是版本控制选项的简短细分:
setup.py
( setuptools ) 中的文件并获取版本。__init__.py
源代码控制),例如bump2version、changes或zest.releaser。__version__
特定模块中的全局变量。setup.py
发布设置值,并使用importlib.metadata在运行时获取它。(警告,有 pre-3.8 和 post-3.8 版本。)__version__
insample/__init__.py
并导入 sample in setup.py
。请注意,(7)可能是最现代的方法(构建元数据独立于代码,由自动化发布)。另请注意,如果 setup 用于包发布,则简单python3 setup.py --version
将直接报告版本。
另外值得注意的是,它也是__version__
一个半标准。在 python 中,__version_info__
这是一个元组,在简单的情况下,您可以执行以下操作:
__version__ = '1.2.3'
__version_info__ = tuple([ int(num) for num in __version__.split('.')])
...并且您可以__version__
从文件或其他任何内容中获取字符串。
似乎没有将版本字符串嵌入 python 包的标准方法。我见过的大多数软件包都使用您的解决方案的一些变体,即 eitner
嵌入版本setup.py
并setup.py
生成一个version.py
仅包含版本信息的模块(例如),由您的包导入,或者
反过来:将版本信息放在包本身中,然后导入以设置版本 setup.py
我还看到了另一种风格:
>>> django.VERSION
(1, 1, 0, 'final', 0)
箭头以一种有趣的方式处理它。
现在(从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__'),
# [...]
)
setuptools
和pbr
管理版本没有标准方法,但管理包的标准方法是setuptools
.
我发现管理版本的最佳解决方案是setuptools
与pbr
扩展一起使用。这是我现在管理版本的标准方式。
为简单的项目设置完整打包的项目可能有点过头了,但是如果您需要管理版本,您可能处于正确的级别来设置所有内容。这样做还可以让你的包在PyPi上发布,这样每个人都可以下载并使用 Pip。
PBR 将大多数元数据从setup.py
工具中移出并放入一个setup.cfg
文件中,该文件随后用作大多数元数据的来源,其中可能包括版本。这允许在需要时使用类似的方式将元数据打包到可执行文件中pyinstaller
(如果需要,您可能需要此信息),并将元数据与其他包管理/设置脚本分开。您可以直接手动更新版本字符串setup.cfg
,它会*.egg-info
在构建包发布时被拉入文件夹。然后,您的脚本可以使用各种方法从元数据访问版本(这些过程在下面的部分中进行了概述)。
将 Git 用于 VCS/SCM 时,此设置会更好,因为它会从 Git 中提取大量元数据,这样您的存储库就可以成为某些元数据的主要真实来源,包括版本、作者、更改日志、等等。对于版本,它将根据 repo 中的 git 标签为当前提交创建一个版本字符串。
setup.py
和setup.cfg
带有元数据的文件。由于 PBR 将直接从您的 git 存储库中提取版本、作者、更改日志和其他信息,因此setup.cfg
可以忽略其中的一些元数据并在为您的包创建分发时自动生成(使用setup.py
)
setuptools
将使用以下方法实时获取最新信息setup.py
:
python setup.py --version
这将setup.cfg
根据所做的最新提交和存储库中存在的标签从文件或 git 存储库中提取最新版本。但是,此命令不会更新发行版中的版本。
setup.py
当您使用(py setup.py sdist
例如,例如)创建分发时,所有当前信息将被提取并存储在分发中。这实质上是运行setup.py --version
命令,然后将该版本信息存储到package.egg-info
存储分发元数据的一组文件中的文件夹中。
更新版本元数据过程的注意事项:
如果您不使用 pbr 从 git 中提取版本数据,那么只需使用新版本信息直接更新您的 setup.cfg(很简单,但请确保这是发布过程的标准部分)。
如果您使用的是 git,并且不需要创建源代码或二进制分发版(使用命令
python setup.py sdist
或其中一个python setup.py bdist_xxx
命令),将 git repo 信息更新到<mypackage>.egg-info
元数据文件夹的最简单方法就是运行python setup.py install
命令。这将运行与从 git 存储库中提取元数据相关的所有 PBR 功能,并更新您的本地.egg-info
文件夹,为您定义的任何入口点安装脚本可执行文件,以及您在运行此命令时可以从输出中看到的其他功能。请注意,该
.egg-info
文件夹通常不会存储在标准 Python.gitignore
文件(例如来自Gitignore.IO)中的 git repo 中,因为它可以从您的源代码生成。如果它被排除在外,请确保您有一个标准的“发布流程”来在发布之前在本地更新元数据,并且您上传到 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
for 包中以提取版本信息,如下所示,类似于其他一些答案:
__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__,
在主文件夹init .py 中:
from .version import __version__
该exec()
函数在任何导入之外运行脚本,因为 setup.py 在导入模块之前运行。您仍然只需要在一个地方管理一个文件中的版本号,但不幸的是它不在 setup.py 中。(这是缺点,但没有导入错误是优点)
此解决方案源自本文
用例 - 通过 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}-{release}{build}
{major}.{minor}.{patch}
[bumpversion:part:release]
optional_value = prod
first_value = dev
values =
dev
prod
[bumpversion:file:packagex/_version.py]
_version.txt
在同一文件夹中创建一个名为 by的文件,__init__.py
并将版本写为一行: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()
我更喜欢从安装环境中读取软件包版本。这是我的src/foo/_version.py
:
from pkg_resources import get_distribution
__version__ = get_distribution('osbc').version
确保foo
始终已安装,这就是为什么src/
需要一个层来防止foo
在未安装的情况下导入。
在 中setup.py
,我使用setuptools-scm自动生成版本。
我在这里描述了一个标准和现代的方式,依靠setuptools_scm
。在过去的几年里,这种模式已经成功地应用于数十个已发布的包,所以我可以热情地推荐它。
请注意,您不需要getversion
包来实现此模式。恰好getversion
文档包含此提示。
version.py
文件中带有__version__ = <VERSION>
参数的文件。在setup.py
文件中导入__version__
参数并将其值放入setup.py
文件中,如下所示:
version=__version__
setup.py
文件version=<CURRENT_VERSION>
- 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__
下的参数,version.py
并像这样更新bumpversion文件:
[bumpversion:file:<RELATIVE_PATH_TO_VERSION_FILE>]
git commit
或git reset
你的 repo 中的所有东西,否则你会得到一个脏 repo 错误。对于它的价值,如果你使用 NumPy distutils,numpy.distutils.misc_util.Configuration
有一个make_svn_version_py()
方法可以将修订号嵌入到package.__svn_version__
变量中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)