有没有一种标准的方式可以将版本字符串与python软件包相关联,从而可以执行以下操作?
import foo
print foo.version
我可以想象有某种方法可以检索数据而无需任何额外的硬编码,因为setup.py
已经指定了次要/主要字符串。另一种解决方案,我发现是有import __version__
我的foo/__init__.py
,然后让__version__.py
所产生的setup.py
。
setup.py
就足够了。看到这个问题。
有没有一种标准的方式可以将版本字符串与python软件包相关联,从而可以执行以下操作?
import foo
print foo.version
我可以想象有某种方法可以检索数据而无需任何额外的硬编码,因为setup.py
已经指定了次要/主要字符串。另一种解决方案,我发现是有import __version__
我的foo/__init__.py
,然后让__version__.py
所产生的setup.py
。
setup.py
就足够了。看到这个问题。
Answers:
不是直接回答您的问题,而是您应该考虑命名它__version__
,而不是version
。
这几乎是一个准标准。标准库中的许多模块都使用__version__
,并且在许多第三方模块中也使用了它,因此它是准标准的。
通常,它__version__
是一个字符串,但有时它也是一个浮点数或元组。
编辑:正如S.Lott所提到的(谢谢!),PEP 8明确表示:
模块级Dunder名称
模块级“dunders”(即名称具有两个前缘和两个纵下划线),例如
__all__
,__author__
,__version__
等应被放置在模块文档字符串之后,但在除了从任何导入语句__future__
进口。
__version_info__
具体?(这“发明”了您自己的双下划线词。)[当詹姆斯发表评论时,下划线在评论中什么也没做,现在它们表明了重点,所以詹姆斯也确实写__version_info__
了。--- ed。]
我使用一个_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将该字符串作为“ version”参数的值传递给setup()
,从而满足功能2的要求。
为了满足功能1,您可以让包(在运行时,而不是在安装时!)从_version文件中导入,myniftyapp/__init__.py
如下所示:
from _version import __version__
这是我使用多年的这种技术的示例。
该示例中的代码稍微复杂一点,但是我在此注释中编写的简化示例应该是完整的实现。
这是导入版本的示例代码。
如果您发现此方法有任何问题,请告诉我。
setup.py
。同样,如果它尝试执行完全深度优先并在执行此操作之前先进行所有深度操作,则如果存在圆形深度操作,则将卡住。但是,如果它在安装依赖项之前尝试构建此软件包,则如果从中导入软件包setup.py
,则不一定能够导入其dep或其deps的正确版本。
execfile("myniftyapp/_version.py")
从setup.py内部进行,而不是尝试手动解析版本代码。在stackoverflow.com/a/2073599/647002中建议-讨论可能也有帮助。
经过十多年的编写Python代码和管理各种程序包的经历,我得出的结论是,DIY可能不是最好的方法。
我开始使用pbr
软件包来处理软件包中的版本控制。如果您将git用作SCM,它将像魔术一样适合您的工作流程,从而节省了数周的工作(您可能会对问题的复杂程度感到惊讶)。
截至目前,pbr在最常用的python软件包中排名第11,并且达到这一水平还没有任何肮脏的技巧:仅仅是一个:用一种非常简单的方法解决了常见的包装问题。
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__,
...
如果您知道另一种似乎更好的方法,请告诉我。
from .version import __version__
setup.py中呢?
setup.py
运行时未加载软件包-试试它,您会收到错误消息(或者至少我做过:
根据递延的PEP 396(模块版本号),有一种建议的方法。它从原理上描述了要遵循的模块的一个(公认的可选)标准。这是一个片段:
3)当一个模块(或包)包括一个版本号时,该版本应该在
__version__
属性中可用。4)对于位于名称空间包中的模块,该模块应包含该
__version__
属性。命名空间包本身不应包含其自己的__version__
属性。5)
__version__
属性的值应该是一个字符串。
尽管这可能为时已晚,但是对于先前的答案有一个稍微简单的替代方法:
__version_info__ = ('1', '2', '3')
__version__ = '.'.join(__version_info__)
(使用来将版本号的自动递增部分转换为字符串将是非常简单的str()
。)
当然,据我所见,人们在使用时通常会使用类似先前提到的版本__version_info__
,并将其存储为int元组;但是,我不太明白这样做的意义,因为我怀疑在某些情况下您会出于好奇或自动递增的目的而出于任何目的对版本号的某些部分执行数学运算,例如对版本号进行加减运算(即使如此,int()
并且str()
可以很容易地使用)。(另一方面,其他人的代码可能期望数字元组而不是字符串元组,从而导致失败。)
当然,这是我自己的观点,我很高兴希望其他人使用数字元组提供输入。
正如shezi提醒我的那样,数字字符串的(词法)比较不一定具有与直接数字比较相同的结果;为此,将需要前导零。因此,最后,将__version_info__
(或将要调用的任何形式)存储为整数值的元组将允许更有效的版本比较。
__version_info__ = (1,2,3)
__version_info__ = (0, 1, 0) __version__ = '.'.join(map(str, __version_info__))
__version__ = '.'.join(__version_info__)
是,__version_info__ = ('1', '2', 'beta')
将成为1.2.beta
不1.2beta
和1.2 beta
这里的许多解决方案都忽略了git
版本标记,这仍然意味着您必须在多个位置跟踪版本(错误)。我通过以下目标实现了这一目标:
git
回购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
目标总是递增第三版数字,但可以使用next_minor_ver
或next_major_ver
递增其他数字。这些命令依赖于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
。
该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
_version.py
应该使用版本控制进行跟踪?
我在包目录中使用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,因为我可以自动管理元数据。
同样值得一提的是,它还__version__
具有半标准性。在python中,__version_info__
这是一个元组,在简单的情况下,您可以执行以下操作:
__version__ = '1.2.3'
__version_info__ = tuple([ int(num) for num in __version__.split('.')])
...您可以__version__
从文件或任何其他内容中获取字符串。
__version_info__
?
__version_info__ = (1, 2, 3)
和__version__ = '.'.join(map(str, __version_info__))
)进行映射更容易。
__version__ = '.'.join(str(i) for i in __version_info__)
稍长,但更pythonic。
i
没有含义,version_num
有点长而模棱两可……)。我什至认为map()
Python 的存在强烈建议在这里使用它,因为我们在这里需要做的是典型的用例map()
(与现有函数一起使用)—我看不到其他许多合理的用法。
似乎没有一种将版本字符串嵌入python包的标准方法。我见过的大多数软件包都使用您的解决方案的某些变体,即eitner
嵌入版本,setup.py
并setup.py
生成version.py
仅包含版本信息的模块(例如),该模块由您的软件包导入,或者
相反:将版本信息放入包本身,然后导入以在其中设置版本 setup.py
箭头以一种有趣的方式处理它。
现在(从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
?
pip install -e .
开发分支上运行或进行测试时还原。setup.py绝对不应依赖于安装过程中的导入软件包来确定部署参数。kes
我还看到了另一种风格:
>>> django.VERSION
(1, 1, 0, 'final', 0)
.VERSION
并不意味着您不必实施__version__
。
django
在项目中实施?
setuptools
和pbr
没有管理版本的标准方法,但是管理软件包的标准方法是setuptools
。
我发现总体上管理版本的最佳解决方案是setuptools
与pbr
扩展一起使用。现在,这是我管理版本的标准方法。
为完整项目设置项目对于简单项目可能是过大的,但是如果您需要管理版本,则可能处于正确的级别来设置所有内容。这样做还可以使您的软件包在PyPi上可发布,因此每个人都可以通过Pip下载和使用它。
PBR将大多数元数据从setup.py
工具中移出,并移到一个setup.cfg
文件中,然后该文件用作大多数元数据的源,其中可以包括版本。这允许使用pyinstaller
所需的类似方法将元数据打包到可执行文件中(如果需要,则可能需要此信息),并将元数据与其他程序包管理/设置脚本分开。您可以直接setup.cfg
手动更新版本字符串,并且*.egg-info
在构建软件包发行版时会将其拉入文件夹。然后,您的脚本可以使用各种方法从元数据访问版本(这些过程在下面的部分中概述)。
将Git用于VCS / SCM时,此设置甚至更好,因为它将从Git中提取很多元数据,这样您的回购就可以成为某些元数据的主要来源,包括版本,作者,变更日志,专门针对版本,它将基于存储库中的git标签为当前提交创建一个版本字符串。
setup.py
和setup.cfg
带有元数据的文件。由于PBR会直接从您的git repo中提取版本,作者,changelog和其他信息,因此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 repo中提取元数据有关的所有PBR功能,并更新本地.egg-info
文件夹,为已定义的任何入口点安装脚本可执行文件,以及运行此命令时从输出中看到的其他功能。请注意,
.egg-info
通常不会将该文件夹存储在git repo本身的标准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__,
在主文件夹init .py中:
from .version import __version__
该exec()
函数在任何导入之外运行脚本,因为setup.py是在导入模块之前运行的。您仍然只需要在一个位置管理一个文件中的版本号,但是不幸的是,它不在setup.py中。(这是不利因素,但没有导入错误是有利因素)
自首次提出这个问题以来,已完成了大量的工作,以统一版本和支持约定。现在,《Python打包用户指南》中详细介绍了可口的选项。同样值得注意的是,按照PEP 440,版本号方案在Python中相对严格,因此,要使程序包发布到Cheese Shop,保持良好状态就至关重要。
以下是版本控制选项的简化分类:
setup.py
(setuptools)中读取文件并获取版本。__init__.py
和源代码控制),例如bump2version,changes或zest.releaser。__version__
为特定模块中的全局变量。setup.py
发行版设置值,并使用importlib.metadata在运行时将其提取。(警告,有3.8之前和3.8之后的版本。)__version__
in sample/__init__.py
并在中导入样本setup.py
。注意,(7)可能是最现代的方法(构建元数据与代码无关,由自动化发布)。另外请注意,如果安装程序用于软件包发行,则简单程序python3 setup.py --version
将直接报告版本。
值得一说的是,如果您使用的是NumPy distutils,numpy.distutils.misc_util.Configuration
则可以使用make_svn_version_py()
一种将修订版号嵌入package.__svn_version__
变量中的方法version
。
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
然后为每个仓库添加一个文件.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
您的回购中的所有内容,否则您将收到肮脏的回购错误。bumpversion
,否则它将无法正常工作。使用最新版本。
version.py
或使用跟踪版本bumpversion
?
__version__
param值带入setup.py文件是更常见的做法。我的解决方案用于生产中,是一种常见的做法。注意:请注意,将bumpversion用作脚本的一部分是最好的解决方案,将其放在您的CI中,它将是自动操作。
如果使用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__
到哪个文件?使用该解决方案,如何增加版本号?