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

时间:2009-01-19 18:05:54

标签: python string package

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

import foo
print foo.version

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

20 个答案:

答案 0 :(得分:109)

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

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

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

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

  

版本簿记

     

如果您的源文件中必须包含Subversion,CVS或RCS crud,   这样做。

    __version__ = "$Revision: 63990 $"
    # $Source$
     

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

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

答案 1 :(得分:98)

我使用单个_version.py文件作为存储版本信息的“曾经的地方”:

  1. 它提供了__version__属性。

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

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

  4. 只有一个地方写下了版本号,因此当版本号发生变化时,只有一个地方可以更改它,版本不一致的可能性也会降低。

  5. 以下是它的工作原理:存储版本号的“一个规范位置”是一个.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__
    

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

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

    这是example code of importing the version

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

答案 2 :(得分:90)

重写的美国

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

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

截至今天pbr is ranked #11 most used python package达到这个水平并没有包含任何肮脏的伎俩:只有一个:以一种非常简单的方式解决常见的包装问题。

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

那么为了让您了解在一次提交中采用pbr的方式看看swiching 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__,
    ...

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

答案 3 :(得分:25)

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

  

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

     

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

     

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

答案 4 :(得分:21)

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

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

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

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

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


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

答案 5 :(得分:10)

我在包dir中使用了一个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,因为我可以自动管理元数据。

答案 6 :(得分:9)

这里的许多解决方案都忽略了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__读取新版本字符串。

  4. 详细说明:

    生成文件

    # 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

    __初始化__。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
    

答案 7 :(得分:7)

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

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

    < / LI>
  2. 相反:将版本信息放入包中,然后导入 以在setup.py

    中设置版本

答案 8 :(得分:6)

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

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

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

答案 9 :(得分:4)

我也看到了另一种风格:

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

答案 10 :(得分:3)

arrow以有趣的方式处理它。

arrow/__init__.py

__version__ = '0.8.0'
VERSION = __version__

setup.py

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

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

答案 11 :(得分:3)

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

在包“ / 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中。 (这是不利的一面,但没有导入错误是好的一面)

答案 12 :(得分:2)

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

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

[bumpversion:file:packagex/_version.py]

答案 13 :(得分:1)

自从首次提出此问题以来,为统一版本控制和支持约定的工作已经完成。 Python Packaging User Guide中的详细信息现在可口的选项。同样值得注意的是version number schemes are relatively strict in Python per PEP 440,因此,要确保您的软件包正常发布到Cheese Shop,至关重要。

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

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

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

答案 14 :(得分:0)

如果您使用的是NumPy distutils,numpy.distutils.misc_util.Configuration有一个make_svn_version_py()方法可以将package.__svn_version__内的修订号嵌入变量version中。< / p>

答案 15 :(得分:0)

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

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

确保foo始终已经安装,这就是为什么需要src/层来防止foo未经安装而导入的原因。

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

答案 16 :(得分:0)

  1. 在与_version.txt相同的文件夹中创建一个以__init__.py命名的文件,并将版本写成一行:
0.8.2
  1. _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()

答案 17 :(得分:-1)

  1. 仅将version.py文件与文件中的__version__ = <VERSION> param一起使用。在setup.py文件中导入__version__参数并将其值放在setup.py文件中,如下所示: version=__version__
  2. 另一种方法是仅使用setup.py version=<CURRENT_VERSION>文件 - CURRENT_VERSION是硬编码的。
  3. 由于我们每次创建新标签(准备发布新的软件包版本)时都不想手动更改文件中的版本,我们可以使用以下内容。

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

    首先将version=<VERSION>添加到setup.py文件中,如果您还没有。{/ p>

    每次碰撞版本时都应该使用这样的简短脚本:

    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 commitgit reset您回购中的所有内容,否则您将收到脏回购错误。
    • 确保您的虚拟环境包含颠倒包,没有它它将无效。

答案 18 :(得分:-1)

使用setuptoolspbr

我发现总体上最好的解决方案是使用setuptoolspbr扩展名。 PBR将大多数元数据从setup.py工具中移出,并移到setup.cfg文件中,然后该文件用作大多数元数据的源,其中可能包括版本。

将Git用于VCS / SCM时,此设置甚至更好,因为它将从Git中提取很多元数据,这样您的回购就可以成为某些元数据(包括版本,作者)的主要来源。 ,变更日志等。

由于PBR会直接从您的git repo中提取版本,作者,变更日志和其他信息,因此,setup.cfg中的某些元数据可以被忽略,并在为您的软件包创建分发时自动生成(使用{ {1}})

实时最新版本

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

setup.py

这将根据所做的最新提交和存储库中存在的标签,从python setup.py --version 文件或git存储库中获取最新版本。但是,此命令不会更新发行版中的版本。

更新版本

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

您可以在包本身的Python脚本中从当前内部版本访问元数据。例如,对于版本,到目前为止,有几种方法可以实现:

package.egg-info

您可以将其中之一直接放入您的## 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)) ## This is my favorite - 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

答案 19 :(得分:-2)

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