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

Dim*_*iuc 241 python string package

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

import foo
print foo.version
Run Code Online (Sandbox Code Playgroud)

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

oef*_*efe 117

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

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

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

编辑:正如S.Lott所说(谢谢!),PEP 8明确地说:

版本簿记

如果您的源文件中必须包含Subversion,CVS或RCS crud,请按以下步骤操作.

    __version__ = "$Revision: 63990 $"
    # $Source$
Run Code Online (Sandbox Code Playgroud)

这些行应该包含在模块的docstring之后,在任何其他代码之前,由上面和下面的空行分隔.

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

  • 它应该是一个字符串,并为元组版本提供__version_info__. (8认同)
  • 对.似乎这些PEP相互矛盾.好吧,PEP 8说"if"和"crud",所以它并不支持使用VCS关键字扩展.此外,如果您切换到不同的VCS,您将丢失修订信息.因此,我建议使用嵌入在单个源文件中的PEP 386/440兼容版本信息,至少对于较大的项目. (2认同)
  • 你会把__version__放在哪里.考虑到这是可接受的版本,我很乐意在这里看到更多信息. (2认同)

Zoo*_*oko 105

我使用单个_version.py文件作为存储版本信息的"one cannonical place":

  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"
Run Code Online (Sandbox Code Playgroud)

所以你的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,))
Run Code Online (Sandbox Code Playgroud)

然后你的setup.py将该字符串作为"version"参数的值传递给setup(),从而满足功能2.

为了满足功能1,你可以拥有你的包(在运行时,而不是在设置时!)从myniftyapp/__init__.py这里导入_version文件:

from _version import __version__
Run Code Online (Sandbox Code Playgroud)

这是我多年来一直使用的这种技术的一个例子.

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

以下是导入版本的示例代码.

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

  • 能否请您描述激发#3的问题?Glyph表示它与"setuptools喜欢在你的setup.py运行时假装你的代码不在系统中的任何地方"有关,但细节将有助于说服我和其他人. (8认同)
  • 类似的方法是从setup.py中的`execfile("myniftyapp/_version.py")`,而不是试图手动解析版本代码.建议在http://stackoverflow.com/a/2073599/647002 - 讨论中也可能有所帮助. (3认同)
  • @Iva现在,该工具应该以什么顺序执行此操作?它不能(在今天的setuptools/pip/virtualenv系统中)甚至知道deps _are_是什么,直到它评估你的`setup.py`.此外,如果它尝试完全深度优先并在执行此操作之前完成所有deps,如果存在圆形deps,它将会卡住.但是如果它在安装依赖项之前尝试构建这个包,那么如果你从`setup.py`导入你的包,它就不一定能够导入它的deps或它的deps的正确版本. (2认同)
  • 你能从"setup.py"写*文件"version.py"而不是解析吗?这似乎更简单. (2认同)
  • Jonathan Hartley:我同意你的"setup.py"编写"version.py"文件而不是解析它会稍微简单一点,但是当你编辑setup.py时会打开一个不一致的窗口.拥有新版本但尚未执行setup.py来更新version.py文件.将规范版本放在一个小的单独文件中的另一个原因是它使得其他*工具(例如读取修订控制状态的工具)可以轻松编写版本文件. (2认同)
  • 应该是`from ._version import __version__`从本地项目中的`_version.py`中引入`_version`模块而不是在你的环境中漂浮的另一个模块. (2认同)

sor*_*rin 96

重写的美国

经过十多年编写Python代码和管理各种软件包后,我得出结论,DIY可能不是最好的方法.

我开始使用pbr包来处理我的包中的版本控制.如果您使用git作为您的SCM,这将适合您的工作流程,如魔术,节省您的工作周(您会惊讶于问题的复杂程度).

截至今天,pbr排名第11位最常用的python包,达到这个级别并没有包含任何肮脏的技巧:只有一个:以一种非常简单的方式解决常见的包装问题.

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

因此,为了让您了解它在一次提交中采用pbr的样子,请查看pbr的swiching包装

可能你会发现版本根本没有存储在存储库中.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'
Run Code Online (Sandbox Code Playgroud)

内部yourpackage/__init__.py:

from .version import __version__
Run Code Online (Sandbox Code Playgroud)

内部setup.py:

exec(open('yourpackage/version.py').read())
setup(
    ...
    version=__version__,
    ...
Run Code Online (Sandbox Code Playgroud)

如果您知道其他方法似乎更好,请告诉我.

  • 呃,不.execfile()在Python 3中不存在,因此最好使用exec(open().read()). (12认同)
  • 为什么不在setup.py中来自.version导入__version__`? (4认同)
  • @Aprillion因为当`setup.py`正在运行时没有加载包 - 试试看,你会得到一个错误(或者至少,我做了:-)) (4认同)
  • 毫无疑问,**pbr**是一个很好的工具,但你没有解决这个问题.如何通过**bpr**访问当前版本或已安装的软件包. (4认同)
  • pbr 是一个可怕的解决方案。这是一个由 openstack 社区为 openstack 问题编写的包,它引入了大量的意见和假设,如果你不知道它们可能会让你大吃一惊。如果你不为 openstack 编写代码,很有可能它引入的问题多于它解决的问题。 (3认同)
  • 工作完美,但添加了绝对路径以使其更加健壮:`here = os.path.abspath(os.path.dirname(__file__))`然后`exec(open(os.path.join(here, 'yourpackage/version. py')).read())`。另外,没有发现需要相对导入。 (2认同)
  • 指向pbr的链接导致网关错误。 (2认同)

Odd*_*ing 29

根据延迟的PEP 396(模块版本号),有一种建议的方法可以做到这一点.它有理由描述了模块的(公认的可选)标准.这是一个片段:

3)当模块(或包)包含版本号时,该__version__属性应该可以使用该版本.

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

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

  • 该PEP不被接受/标准化,但是被推迟(由于缺乏兴趣).因此,说明"*有一种标准方式*"是有点误导性的. (11认同)
  • 编辑注意它不是标准.现在我感到很尴尬,因为我已经提出要求他们遵循这个"标准"的项目的功能请求. (3认同)

JAB*_*JAB 21

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

__version_info__ = ('1', '2', '3')
__version__ = '.'.join(__version_info__)
Run Code Online (Sandbox Code Playgroud)

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

当然,从我所看到的情况来看,人们在使用时倾向于使用类似于前面提到的版本的东西__version_info__,因此将其存储为整数元组; 但是,我没有完全看到这样做的重点,因为我怀疑在某些情况下你会为了除了好奇心或自动增量之外的任何目的在版本号的部分上执行数学运算,例如加法和减法(即便如此,int()并且str()可以相当容易地使用).(另一方面,有可能其他人的代码期望数字元组而不是字符串元组因此失败.)

当然,这是我自己的观点,我很乐意让其他人投入使用数字元组.


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

  • 我这样做很好,但略有调整,以解决整数..`__version_info__ =(0,1,0)= __version__ ''.加入(图(STR,__version_info __))` (13认同)
  • 很好(+1),但你不喜欢数字而不是字符串?例如`__version_info__ =(1,2,3)` (12认同)
  • 我认为这种方法的问题在于在哪里放置代码行来声明\ _\_ _ version__.如果它们在setup.py中,那么您的程序无法从其包中导入它们.也许这对你来说不是问题,在这种情况下,很好.如果它们进入你的程序,那么你的setup.py可以导入它们,但它不应该,因为setup.py在安装期间运行时尚未安装程序的依赖项(setup.py用于确定依赖项是什么) .)因此Zooko的答案是:手动解析产品源文件中的值,而不导入产品包 (4认同)
  • 当版本号超过9时,字符串的比较会变得危险,因为例如'10'<'2'. (3认同)
  • `__version__ = '.'.join(__version_info__)` 的问题是 `__version_info__ = ('1', '2', 'beta')` 将变成 `1.2.beta`,而不是 `1.2beta` 或 `1.2 beta ` (2认同)

ing*_*ere 12

自从第一次提出这个问题以来,关于统一版本控制和支持约定的大量工作已经完成。《Python 打包用户指南》中现已详细介绍了可口的选项。另外值得注意的是,Python 中的版本号方案相对严格(按照 PEP 440) ,因此如果您的软件包将发布到Cheese Shop,保持理智至关重要。

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

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

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


And*_*Lee 10

我在包dir中使用了一个JSON文件.这符合Zooko的要求.

内部pkg_dir/pkg_info.json:

{"version": "0.1.0"}
Run Code Online (Sandbox Code Playgroud)

内部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'],
    ...
    )
Run Code Online (Sandbox Code Playgroud)

内部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']
Run Code Online (Sandbox Code Playgroud)

我还提供其他信息pkg_info.json,如作者.我喜欢使用JSON,因为我可以自动管理元数据.


cmc*_*nty 10

这里的许多解决方案都忽略了git版本标签,这仍然意味着你必须在多个地方跟踪版本(糟糕).我实现了以下目标:

  • gitrepo中的标记派生所有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
Run Code Online (Sandbox Code Playgroud)

release目标总是递增第三版数字,但可以使用next_minor_vernext_major_ver递增其他数字.命令依赖于versionbump.py检入repo根目录的脚本

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
Run Code Online (Sandbox Code Playgroud)

这将解决如何处理和增加版本号的问题git.

__init__.py

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

from ._version import __version__
__author__ = ''
__email__ = ''
Run Code Online (Sandbox Code Playgroud)

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__'],
    ...
    ...
)
Run Code Online (Sandbox Code Playgroud)

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

git tag -a v0.0.1
Run Code Online (Sandbox Code Playgroud)

  • 为什么不应该使用版本控制来跟踪“_version.py”? (2认同)

Jam*_*ill 6

另外值得注意的是,这也是__version__一个半标准.在python中所以__version_info__它是一个元组,在简单的情况下你可以做类似的事情:

__version__ = '1.2.3'
__version_info__ = tuple([ int(num) for num in __version__.split('.')])
Run Code Online (Sandbox Code Playgroud)

...你可以__version__从文件或其他任何东西中获取字符串.

  • 更容易在另一个方向上进行映射(`__version_info__ =(1,2,3)`和`__version__ ='.'.join(map(str,__ version_info __))`). (6认同)
  • 好吧,它与sys.version到sys.version_info的映射相同.所以:http://docs.python.org/library/sys.html#sys.version_info (3认同)
  • @EOL - `__version__ ='.'.join(str(i)for i in __version_info __)` - 稍长但更pythonic. (2认同)
  • 我不确定你的建议是否明显更像 pythonic,因为它包含一个虚拟变量,它不是真正需要的,其含义有点难以表达(`i` 没有意义,`version_num` 有点长而且模棱两可……)。我什至认为 Python 中存在 `map()` 作为强烈暗示应该在这里使用它,因为我们在这里需要做的是 `map()` 的典型用例(与现有函数一起使用)——我没有看到许多其他合理的用途。 (2认同)

dF.*_*dF. 5

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

  1. 将版本嵌入setup.pysetup.py生成一个模块(例如version.py),其中仅包含由您的软件包导入的版本信息,或者

  2. 反过来:将版本信息放入包中,然后导入以设置版本 setup.py


L1k*_*ker 5

我还看到了另一种风格:

>>> django.VERSION
(1, 1, 0, 'final', 0)
Run Code Online (Sandbox Code Playgroud)


Ant*_*nto 5

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

现在(从2e5031b 开始

arrow/__init__.py

__version__ = 'x.y.z'
Run Code Online (Sandbox Code Playgroud)

setup.py

from arrow import __version__

setup(
    name='arrow',
    version=__version__,
    # [...]
)
Run Code Online (Sandbox Code Playgroud)

arrow/__init__.py

__version__ = 'x.y.z'
VERSION = __version__
Run Code Online (Sandbox Code Playgroud)

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__'),
    # [...]
)
Run Code Online (Sandbox Code Playgroud)

  • 更新的解决方案实际上是有害的。当setup.py运行时,它不一定会从本地文件路径中看到包的版本。它可能会恢复到以前安装的版本,例如在测试时在开发分支上运行“pip install -e”。setup.py 绝对不应该依赖于导入安装过程中的包来确定部署参数。哎呀。 (2认同)

Lig*_*tCC 5

使用setuptoolspbr

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

setuptools我发现总体上管理版本的最佳解决方案是与扩展一起使用pbr。现在这是我管理版本的标准方法。

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

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

当将 Git 用于 VCS/SCM 时,此设置甚至更好,因为它将从 Git 中提取大量元数据,以便您的存储库可以成为某些元数据的主要真实来源,包括版本、作者、更改日志、具体来说,对于版本,它将根据存储库中的 git 标签为当前提交创建版本字符串。

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



实时获取当前版本

setuptools将使用以下方式实时提取最新信息setup.py

python setup.py --version
Run Code Online (Sandbox Code Playgroud)

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 存储库信息更新到元数据文件夹的最简单方法<mypackage>.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))
Run Code Online (Sandbox Code Playgroud)

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

__all__ = (
    '__version__',
    'my_package_name'
)

import pkg_resources  # part of setuptools

__version__ = pkg_resources.get_distribution("mypackage").version
Run Code Online (Sandbox Code Playgroud)