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

2020/10/31 17:11 · python ·  · 0评论

有没有一种标准的方式可以将版本字符串与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__进口。

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

我使用一个_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,您可以让您的包(在运行时,而不是在安装时!)从_version文件中导入,myniftyapp/__init__.py如下所示:

from _version import __version__

这是我使用多年的这种技术的示例

该示例中的代码稍微复杂一些,但是我在此注释中编写的简化示例应该是完整的实现。

这是导入版本的示例代码

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

改写2017-05

经过13年以上的编写Python代码和管理各种程序包的经验,我得出的结论是,DIY可能不是最好的方法。

我开始使用该pbr软件包来处理软件包中的版本控制。如果您将git用作SCM,它将像魔术一样适合您的工作流程,从而节省了数周的工作(您可能会对问题的复杂程度感到惊讶)。

截至今天,它pbr是第11个最常用的python软件包,达到这一水平还没有任何肮脏的把戏。这只是一件事-以非常简单的方式解决常见的包装问题。

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元组。但是,我不太明白这样做的意义,因为我怀疑在某些情况下您会出于好奇或自动递增的目的而出于任何目的对版本号的某些部分执行数学运算,例如对版本号进行加减运算(即使如此,int()并且str()可以很容易地使用)。(另一方面,其他人的代码可能期望数字元组而不是字符串元组,从而导致失败。)

当然,这是我自己的观点,我很高兴希望其他人使用数字元组提供输入。


正如shezi提醒我的那样,数字字符串的(词法)比较不一定具有与直接数字比较相同的结果;为此,将需要前导零。因此,最后,将__version_info__(或将要调用的任何形式)存储为整数值的元组将允许更有效的版本比较。

这里的许多解决方案都忽略了git版本标记,这仍然意味着您必须在多个位置跟踪版本(错误)。我通过以下目标实现了这一目标:

  • 派生的从标签的所有Python版本引用git回购
  • 使用一个无需输入的命令自动执行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__

细节:

生成文件

# 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递增其他数字。这些命令依赖于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

我在包目录中使用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__从文件或任何其他内容中获取字符串。

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

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

  2. 相反:将版本信息放入包本身,然后导入在其中设置版本 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__'),
    # [...]
)

我还看到了另一种风格:

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

使用setuptoolspbr

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

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

为完整项目设置项目对于简单项目可能是过大的,但是如果您需要管理版本,则可能处于正确的级别来设置所有内容。这样做还可以使您的软件包在PyPi上发布,因此每个人都可以通过Pip下载和使用它。

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

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

由于PBR会直接从您的git repo中提取版本,作者,changelog和其他信息,因此setup.cfg每当为您的软件包创建分发时(使用setup.py可以省去部分元数据并自动生成这些元数据


实时获取当前版本

setuptools将使用setup.py以下命令实时获取最新信息

python setup.py --version

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


更新版本元数据

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

关于更新版本元数据的过程的注释:

如果您不使用pbr从git中提取版本数据,则只需使用新的版本信息直接更新setup.cfg(这很容易,但是请确保这是发布过程的标准部分)。

如果您使用的是git,而无需创建源代码或二进制发行版(使用python setup.py sdistpython setup.py bdist_xxx命令之一),则将git repo信息更新到<mypackage>.egg-info元数据文件夹中的最简单方法就是运行python setup.py install命令。这将运行与从git repo中提取元数据有关的所有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包中以提取版本信息,如下所示,类似于其他答案:

__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,保持良好状态就至关重要

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

  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__insample/__init__.py在中导入样本setup.py
  7. 使用setuptools_scm从源代码管理中提取版本控制,以便它是规范的参考,而不是代码。

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

  1. 使用一个version.py文件只用__version__ = <VERSION>该文件在参数。setup.py文件中导入__version__参数,然后将其值放在setup.py文件中,如下所示:
    version=__version__
  2. 另一种方法是仅使用-CURRENT_VERSION进行硬编码setup.py文件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 commitgit reset你的回购一切,否则你会得到一个肮脏的回购错误。
  • 确保您的虚拟环境中包含了bumpversion软件包,如果没有,将无法使用。

值得一说的是,如果您使用的是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)
本文地址:http://python.askforanswer.com/jiangbanbenqianrupythonbaodebiaozhunfangfa.html
文章标签: ,   ,  
版权声明:本文为原创文章,版权归 admin 所有,欢迎分享本文,转载请保留出处!

文件下载

老薛主机终身7折优惠码boke112

上一篇:
下一篇:

评论已关闭!