将版本embeddedpython包的标准方法?

有没有一种标准的方式来关联版本string与python包,以这样的方式,我可以做到以下几点?

import foo print foo.version 

我想可以通过一些方法来检索数据,而不需要任何额外的硬编码,因为已经在setup.py中指定了小/主string。 我find的替代解决scheme是在我的foo/__init__.py import __version__ foo/__init__.py ,然后由setup.py生成__version__.py

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

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

通常, __version__是一个string,但有时它也是一个浮点数或元组。

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

版本簿记

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

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

这些行应该包含在模块的文档string之后,在任何其他代码之前,在上面和下面用空行分隔。

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

这是我如何做到这一点。 以下方法的优点:

  1. 它提供了__version__属性。

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

  3. 在构build包时,它不会导入包(或其他任何东西),这在某些情况下会导致问题。 (请参阅下面关于这可能导致什么问题的评论。)

  4. 版本号只有一个地方写下来,版本号变化时只有一个地方可以更改,版本不一致的机会较less。

下面是它是如何工作的:存储版本号的“一个规范的地方”是一个.py文件,名为“_version.py”,它位于Python包中,例如在myniftyapp/_version.py 。 这个文件是一个Python模块,但你的setup.py不会导入它! (这将打败function3.)相反,你的setup.py知道这个文件的内容是非常简单的,就像:

 __version__ = "3.6.5" 

所以你的setup.py打开文件并parsing它,代码如下:

 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将该string作为setup()的“version”参数的值传递,从而满足特性2。

为了满足特性1,你可以让你的包(在运行时,而不是在安装时!)从myniftyapp/__init__.py导入_version文件,如下所示:

 from _version import __version__ 

这是我已经使用多年的这种技术的一个例子 。

这个例子中的代码有点复杂,但是我写入这个注释的简单例子应该是一个完整的实现。

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

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

重写了2017-05

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

我开始使用pbr软件包来处理软件包中的版本控制。 如果你使用git作为你的SCM,这将会像magic一样适合你的工作stream程,节省你的工作周(你会惊讶于这个问题有多复杂)。

截至今天, pbr是排名第11的最常用的python软件包 ,达到这个水平并没有包含任何肮脏的技巧:只有一个:以一种非常简单的方式修复常见的包装问题。

pbr可以做更多的包维护负担,并不局限于版本控制,但并不强迫你采取所有的好处。

所以给你一个关于如何在一次提交中采用pbr的想法,看看这个开发包

可能你会发现版本根本不存储在版本库中。 PBR确实从Git分支和标签中检测到它。

不需要担心没有git存储库会发生什么情况,因为在打包或安装应用程序时,pbr会“编译”并caching版本,所以git没有运行时依赖。

旧解决scheme

这是迄今为止我见过的最好的解决scheme,它也解释了为什么:

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(模块版本号码) ,有一个build议的方法来做到这一点。 它以理由描述了模块要遵循的(可以select的)标准。 这是一个片段:

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

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

5) __version__属性的值应该是一个string。

虽然这可能为时已晚,但对于以前的答案来说,有一个更简单的select:

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

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

当然,从我所看到的,当使用__version_info__时,人们倾向于使用前面提到的版本,并将其作为整数存储; 然而,我并不完全明白这一点,因为我怀疑有些情况下,除了好奇心或自动增量外,还会执行诸如版本号的部分加减等math运算(即使如此, int()str()可以相当容易地使用)。 (另一方面,有可能是别人的代码期望一个数字元组而不是一个string元组,从而失败。)

这当然是我自己的看法,我很乐意喜欢别人对使用数字元组的意见。


正如她提醒我的,数字串的(词汇)比较不一定与直接数字比较具有相同的结果; 领导零将需要提供这一点。 所以最后,将__version_info__ (或者其他被调用的)存储为整数值的元组将会允许更有效的版本比较。

我在包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,因为我可以自动pipe理元数据。

似乎没有一种将版本stringembedded到python包中的标准方法。 我见过的大多数软件包都使用了你的解决scheme的一些变种,比如eitner

  1. setup.pyembedded版本并让setup.py生成一个只包含版本信息的模块(例如version.py

  2. 相反:把版本信息放在你自己的包里,然后在setup.py导入

我也看到了另一种风格:

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

还值得注意的是,以及__version__是一个半标准的。 在Python中,所以是__version_info__这是一个元组,在简单的情况下,你可以做这样的事情:

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

…你可以从文件中获得__version__string,或者其他任何东西。

这些解决scheme中的许多忽略git版本标签,这仍然意味着你必须跟踪版本在多个地方(坏)。 我以下面的目标来实现这个目标:

  • git仓库中的标签派生所有的Python版本引用
  • 使用不带input的单一命令自动执行git tag / pushsetup.py upload步骤。

怎么运行的:

  1. make release命令中,find并增加了git仓库中的最后一个标记版本。 标签被推回origin

  2. Makefile将版本存储在src/_version.py中,并由setup.py读取,同时也包含在版本中。 不要检查_version.py到源代码pipe理中!

  3. setup.py命令从package.__version__读取新的版本string。

细节:

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 

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 。 把你想要分发给你的模块的任何静态安assembly置。

 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__'], ... ... ) 

当然,为了所有这些工作,你必须在你的仓库至less有一个版本标签才能启动。

 git tag -a v0.0.1 

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

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__'), # [...] ) 

如果您使用的是NumPy distutils, numpy.distutils.misc_util.Configuration有一个make_svn_version_py()方法,它将版本号embedded到variablesversion package.__svn_version__中。

如果您使用CVS(或RCS)并想要快速解决scheme,则可以使用:

 __version__ = "$Revision: 1.1 $"[11:-2] __version_info__ = tuple([int(s) for s in __version__.split(".")]) 

(当然,CVS会replace您的版本号。)

这为您提供了一个便于打印的版本和一个版本信息,您可以使用它来检查您正在导入的模块是否至less具有预期的版本:

 import my_module assert my_module.__version_info__ >= (1, 1)