将版本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)中描述的格式。
这是我如何做到这一点。 以下方法的优点:
-
它提供了
__version__
属性。 -
它提供了标准的元数据版本。 因此,它将被
pkg_resources
或parsing包元数据(EGG-INFO和/或PKG-INFO,PEP 0345)的其他工具检测到。 -
在构build包时,它不会导入包(或其他任何东西),这在某些情况下会导致问题。 (请参阅下面关于这可能导致什么问题的评论。)
-
版本号只有一个地方写下来,版本号变化时只有一个地方可以更改,版本不一致的机会较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
-
在
setup.py
embedded版本并让setup.py
生成一个只包含版本信息的模块(例如version.py
) -
相反:把版本信息放在你自己的包里,然后在
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
/push
和setup.py upload
步骤。
怎么运行的:
-
从
make release
命令中,find并增加了git仓库中的最后一个标记版本。 标签被推回origin
。 -
Makefile
将版本存储在src/_version.py
中,并由setup.py
读取,同时也包含在版本中。 不要检查_version.py
到源代码pipe理中! -
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_ver
或next_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)