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

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

有没有一种标准的方式可以将版本字符串与python软件包相关联,从而可以执行以下操作?

import foo
print foo.version

我可以想象有某种方法可以检索数据而无需任何额外的硬编码,因为setup.py已经指定了次要/主要字符串。另一种解决方案,我发现是有import __version__我的foo/__init__.py,然后让__version__.py所产生的setup.py

Is there a standard way to associate version string with a python package in such way that I could do the following?

import foo
print foo.version

I would imagine there’s some way to retrieve that data without any extra hardcoding, since minor/major strings are specified in setup.py already. Alternative solution that I found was to have import __version__ in my foo/__init__.py and then have __version__.py generated by setup.py.


回答 0

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

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

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

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

模块级Dunder名称

模块级“dunders”(即名称具有两个前缘和两个纵下划线),例如__all____author____version__等应被放置在模块文档字符串之后,但在除了从任何导入语句__future__进口。

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

Not directly an answer to your question, but you should consider naming it __version__, not version.

This is almost a quasi-standard. Many modules in the standard library use __version__, and this is also used in lots of 3rd-party modules, so it’s the quasi-standard.

Usually, __version__ is a string, but sometimes it’s also a float or tuple.

Edit: as mentioned by S.Lott (Thank you!), PEP 8 says it explicitly:

Module Level Dunder Names

Module level “dunders” (i.e. names with two leading and two trailing underscores) such as __all__, __author__, __version__, etc. should be placed after the module docstring but before any import statements except from __future__ imports.

You should also make sure that the version number conforms to the format described in PEP 440 (PEP 386 a previous version of this standard).


回答 1

我使用一个_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__

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

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

这是导入版本的示例代码

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

I use a single _version.py file as the “once cannonical place” to store version information:

  1. It provides a __version__ attribute.

  2. It provides the standard metadata version. Therefore it will be detected by pkg_resources or other tools that parse the package metadata (EGG-INFO and/or PKG-INFO, PEP 0345).

  3. It doesn’t import your package (or anything else) when building your package, which can cause problems in some situations. (See the comments below about what problems this can cause.)

  4. There is only one place that the version number is written down, so there is only one place to change it when the version number changes, and there is less chance of inconsistent versions.

Here is how it works: the “one canonical place” to store the version number is a .py file, named “_version.py” which is in your Python package, for example in myniftyapp/_version.py. This file is a Python module, but your setup.py doesn’t import it! (That would defeat feature 3.) Instead your setup.py knows that the contents of this file is very simple, something like:

__version__ = "3.6.5"

And so your setup.py opens the file and parses it, with code like:

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,))

Then your setup.py passes that string as the value of the “version” argument to setup(), thus satisfying feature 2.

To satisfy feature 1, you can have your package (at run-time, not at setup time!) import the _version file from myniftyapp/__init__.py like this:

from _version import __version__

Here is an example of this technique that I’ve been using for years.

The code in that example is a bit more complicated, but the simplified example that I wrote into this comment should be a complete implementation.

Here is example code of importing the version.

If you see anything wrong with this approach, please let me know.


回答 2

改写2017-05

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

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

截至目前,pbr在最常用的python软件包中排名第11,并且达到这一水平还没有任何肮脏的技巧:仅仅是一个:用一种非常简单的方法解决了常见的包装问题。

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__,
    ...

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

Rewritten 2017-05

After 13+ years of writing Python code and managing various packages, I came to the conclusion that DIY is maybe not the best approach.

I started using the pbr package for dealing with versioning in my packages. If you are using git as your SCM, this will fit into your workflow like magic, saving your weeks of work (you will be surprised about how complex the issue can be).

As of today, pbr is the 11th most used python package, and reaching this level didn’t include any dirty tricks. It was only one thing — fixing a common packaging problem in a very simple way.

pbr can do more of the package maintenance burden, and is not limited to versioning, but it does not force you to adopt all its benefits.

So to give you an idea about how it looks to adopt pbr in one commit have a look switching packaging to pbr

Probably you would observed that the version is not stored at all in the repository. PBR does detect it from Git branches and tags.

No need to worry about what happens when you do not have a git repository because pbr does “compile” and cache the version when you package or install the applications, so there is no runtime dependency on git.

Old solution

Here is the best solution I’ve seen so far and it also explains why:

Inside 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'

Inside yourpackage/__init__.py:

from .version import __version__

Inside setup.py:

exec(open('yourpackage/version.py').read())
setup(
    ...
    version=__version__,
    ...

If you know another approach that seems to be better let me know.


回答 3

根据递延的PEP 396(模块版本号),有一种建议的方法。它从原理上描述了要遵循的模块的一个(公认的可选)标准。这是一个片段:

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

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

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

Per the deferred PEP 396 (Module Version Numbers), there is a proposed way to do this. It describes, with rationale, an (admittedly optional) standard for modules to follow. Here’s a snippet:

3) When a module (or package) includes a version number, the version SHOULD be available in the __version__ attribute.

4) For modules which live inside a namespace package, the module SHOULD include the __version__ attribute. The namespace package itself SHOULD NOT include its own __version__ attribute.

5) The __version__ attribute’s value SHOULD be a string.


回答 4

尽管这可能为时已晚,但是对于先前的答案有一个稍微简单的替代方法:

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

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

当然,据我所见,人们在使用时通常会使用类似先前提到的版本__version_info__,并将其存储为int元组;但是,我不太明白这样做的意义,因为我怀疑在某些情况下您会出于好奇或自动递增的目的而出于任何目的对版本号的某些部分执行数学运算,例如对版本号进行加减运算(即使如此,int()并且str()可以很容易地使用)。(另一方面,其他人的代码可能期望数字元组而不是字符串元组,从而导致失败。)

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


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

Though this is probably far too late, there is a slightly simpler alternative to the previous answer:

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

(And it would be fairly simple to convert auto-incrementing portions of version numbers to a string using str().)

Of course, from what I’ve seen, people tend to use something like the previously-mentioned version when using __version_info__, and as such store it as a tuple of ints; however, I don’t quite see the point in doing so, as I doubt there are situations where you would perform mathematical operations such as addition and subtraction on portions of version numbers for any purpose besides curiosity or auto-incrementation (and even then, int() and str() can be used fairly easily). (On the other hand, there is the possibility of someone else’s code expecting a numerical tuple rather than a string tuple and thus failing.)

This is, of course, my own view, and I would gladly like others’ input on using a numerical tuple.


As shezi reminded me, (lexical) comparisons of number strings do not necessarily have the same result as direct numerical comparisons; leading zeroes would be required to provide for that. So in the end, storing __version_info__ (or whatever it would be called) as a tuple of integer values would allow for more efficient version comparisons.


回答 5

这里的许多解决方案都忽略了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

Many of these solutions here ignore git version tags which still means you have to track version in multiple places (bad). I approached this with the following goals:

  • Derive all python version references from a tag in the git repo
  • Automate git tag/push and setup.py upload steps with a single command that takes no inputs.

How it works:

  1. From a make release command, the last tagged version in the git repo is found and incremented. The tag is pushed back to origin.

  2. The Makefile stores the version in src/_version.py where it will be read by setup.py and also included in the release. Do not check _version.py into source control!

  3. setup.py command reads the new version string from package.__version__.

Details:

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

The release target always increments the 3rd version digit, but you can use the next_minor_ver or next_major_ver to increment the other digits. The commands rely on the versionbump.py script that is checked into the root of the 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

This does the heavy lifting how to process and increment the version number from git.

__init__.py

The my_module/_version.py file is imported into my_module/__init__.py. Put any static install config here that you want distributed with your module.

from ._version import __version__
__author__ = ''
__email__ = ''

setup.py

The last step is to read the version info from the my_module 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__'],
    ...
    ...
)

Of course, for all of this to work you’ll have to have at least one version tag in your repo to start.

git tag -a v0.0.1

回答 6

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

I use a JSON file in the package dir. This fits Zooko’s requirements.

Inside pkg_dir/pkg_info.json:

{"version": "0.1.0"}

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

Inside 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']

I also put other information in pkg_info.json, like author. I like to use JSON because I can automate management of metadata.


回答 7

同样值得一提的是,它还__version__具有半标准性。在python中,__version_info__这是一个元组,在简单的情况下,您可以执行以下操作:

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

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

Also worth noting is that as well as __version__ being a semi-std. in python so is __version_info__ which is a tuple, in the simple cases you can just do something like:

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

…and you can get the __version__ string from a file, or whatever.


回答 8

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

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

  2. 相反:将版本信息放入包本身,然后导入在其中设置版本 setup.py

There doesn’t seem to be a standard way to embed a version string in a python package. Most packages I’ve seen use some variant of your solution, i.e. eitner

  1. Embed the version in setup.py and have setup.py generate a module (e.g. version.py) containing only version info, that’s imported by your package, or

  2. The reverse: put the version info in your package itself, and import that to set the version in setup.py


回答 9

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

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

arrow handles it in an interesting way.

Now (since 2e5031b)

In arrow/__init__.py:

__version__ = 'x.y.z'

In setup.py:

from arrow import __version__

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

Before

In arrow/__init__.py:

__version__ = 'x.y.z'
VERSION = __version__

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

回答 10

我还看到了另一种风格:

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

I also saw another style:

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

回答 11

使用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通常不会将该文件夹存储在git repo本身的标准Python .gitignore文件中(例如,来自Gitignore.IO),因为可以从您的源中生成该文件夹。如果不包括在内,请确保您具有标准的“发布过程”以在发布之前在本地更新元数据,并且您上载到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

Using setuptools and pbr

There is not a standard way to manage version, but the standard way to manage your packages is setuptools.

The best solution I’ve found overall for managing version is to use setuptools with the pbr extension. This is now my standard way of managing version.

Setting up your project for full packaging may be overkill for simple projects, but if you need to manage version, you are probably at the right level to just set everything up. Doing so also makes your package releasable at PyPi so everyone can download and use it with Pip.

PBR moves most metadata out of the setup.py tools and into a setup.cfg file that is then used as a source for most metadata, which can include version. This allows the metadata to be packaged into an executable using something like pyinstaller if needed (if so, you will probably need this info), and separates the metadata from the other package management/setup scripts. You can directly update the version string in setup.cfg manually, and it will be pulled into the *.egg-info folder when building your package releases. Your scripts can then access the version from the metadata using various methods (these processes are outlined in sections below).

When using Git for VCS/SCM, this setup is even better, as it will pull in a lot of the metadata from Git so that your repo can be your primary source of truth for some of the metadata, including version, authors, changelogs, etc. For version specifically, it will create a version string for the current commit based on git tags in the repo.

As PBR will pull version, author, changelog and other info directly from your git repo, so some of the metadata in setup.cfg can be left out and auto generated whenever a distribution is created for your package (using setup.py)



Get the current version in real-time

setuptools will pull the latest info in real-time using setup.py:

python setup.py --version

This will pull the latest version either from the setup.cfg file, or from the git repo, based on the latest commit that was made and tags that exist in the repo. This command doesn’t update the version in a distribution though.



Updating the version metadata

When you create a distribution with setup.py (i.e. py setup.py sdist, for example), then all the current info will be extracted and stored in the distribution. This essentially runs the setup.py --version command and then stores that version info into the package.egg-info folder in a set of files that store distribution metadata.

Note on process to update version meta-data:

If you are not using pbr to pull version data from git, then just update your setup.cfg directly with new version info (easy enough, but make sure this is a standard part of your release process).

If you are using git, and you don’t need to create a source or binary distribution (using python setup.py sdist or one of the python setup.py bdist_xxx commands) the simplest way to update the git repo info into your <mypackage>.egg-info metadata folder is to just run the python setup.py install command. This will run all the PBR functions related to pulling metadata from the git repo and update your local .egg-info folder, install script executables for any entry-points you have defined, and other functions you can see from the output when you run this command.

Note that the .egg-info folder is generally excluded from being stored in the git repo itself in standard Python .gitignore files (such as from Gitignore.IO), as it can be generated from your source. If it is excluded, make sure you have a standard “release process” to get the metadata updated locally before release, and any package you upload to PyPi.org or otherwise distribute must include this data to have the correct version. If you want the Git repo to contain this info, you can exclude specific files from being ignored (i.e. add !*.egg-info/PKG_INFO to .gitignore)



Accessing the version from a script

You can access the metadata from the current build within Python scripts in the package itself. For version, for example, there are several ways to do this I have found so far:

## 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))

You can put one of these directly in your __init__.py for the package to extract the version info as follows, similar to some other answers:

__all__ = (
    '__version__',
    'my_package_name'
)

import pkg_resources  # part of setuptools

__version__ = pkg_resources.get_distribution("mypackage").version

回答 12

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

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

After several hours of trying to find the simplest reliable solution, here are the parts:

create a version.py file INSIDE the folder of your package “/mypackage”:

# 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'

in setup.py:

exec(open('mypackage/version.py').read())
setup(
    name='mypackage',
    version=__version__,

in the main folder init.py:

from .version import __version__

The exec() function runs the script outside of any imports, since setup.py is run before the module can be imported. You still only need to manage the version number in one file in one place, but unfortunately it is not in setup.py. (that’s the downside, but having no import bugs is the upside)


回答 13

自首次提出这个问题以来,已完成了大量的工作,以统一版本和支持约定。现在,《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__in sample/__init__.py并在中导入样本setup.py
  7. 使用setuptools_scm从源代码管理中提取版本控制,以便它是规范参考,而不是代码。

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

Lots of work toward uniform versioning and in support of conventions has been completed since this question was first asked. Palatable options are now detailed in the Python Packaging User Guide. Also noteworthy is that version number schemes are relatively strict in Python per PEP 440, and so keeping things sane is critical if your package will be released to the Cheese Shop.

Here’s a shortened breakdown of versioning options:

  1. Read the file in setup.py (setuptools) and get the version.
  2. Use an external build tool (to update both __init__.py as well as source control), e.g. bump2version, changes or zest.releaser.
  3. Set the value to a __version__ global variable in a specific module.
  4. Place the value in a simple VERSION text file for both setup.py and code to read.
  5. Set the value via a setup.py release, and use importlib.metadata to pick it up at runtime. (Warning, there are pre-3.8 and post-3.8 versions.)
  6. Set the value to __version__ in sample/__init__.py and import sample in setup.py.
  7. Use setuptools_scm to extract versioning from source control so that it’s the canonical reference, not code.

NOTE that (7) might be the most modern approach (build metadata is independent of code, published by automation). Also NOTE that if setup is used for package release that a simple python3 setup.py --version will report the version directly.


回答 14

值得一说的是,如果您使用的是NumPy distutils,numpy.distutils.misc_util.Configuration则可以使用make_svn_version_py()一种将修订版号嵌入package.__svn_version__变量中的方法version

For what it’s worth, if you’re using NumPy distutils, numpy.distutils.misc_util.Configuration has a make_svn_version_py() method that embeds the revision number inside package.__svn_version__ in the variable version .


回答 15

  1. 使用一个version.py文件只用__version__ = <VERSION>该文件在参数。在setup.py文件中导入__version__参数,然后将其值放在setup.py文件中,如下所示: version=__version__
  2. 另一种方法是仅使用setup.py带有version=<CURRENT_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程序包,如果没有,它将无法正常工作。
  1. Use a version.py file only with __version__ = <VERSION> param in the file. In the setup.py file import the __version__ param and put it’s value in the setup.py file like this: version=__version__
  2. Another way is to use just a setup.py file with version=<CURRENT_VERSION> – the CURRENT_VERSION is hardcoded.

Since we don’t want to manually change the version in the file every time we create a new tag (ready to release a new package version), we can use the following..

I highly recommend bumpversion package. I’ve been using it for years to bump a version.

start by adding version=<VERSION> to your setup.py file if you don’t have it already.

You should use a short script like this every time you bump a version:

bumpversion (patch|minor|major) - choose only one option
git push
git push --tags

Then add one file per repo called: .bumpversion.cfg:

[bumpversion]
current_version = <CURRENT_TAG>
commit = True
tag = True
tag_name = {new_version}
[bumpversion:file:<RELATIVE_PATH_TO_SETUP_FILE>]

Note:

  • You can use __version__ parameter under version.py file like it was suggested in other posts and update the bumpversion file like this: [bumpversion:file:<RELATIVE_PATH_TO_VERSION_FILE>]
  • You must git commit or git reset everything in your repo, otherwise you’ll get a dirty repo error.
  • Make sure that your virtual environment includes the package of bumpversion, without it it will not work.

回答 16

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

If you use CVS (or RCS) and want a quick solution, you can use:

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

(Of course, the revision number will be substituted for you by CVS.)

This gives you a print-friendly version and a version info that you can use to check that the module you are importing has at least the expected version:

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