单一来源的包版本#

待办事项

为除 setuptools 之外的构建后端更新此页面。

有许多技术可以维护项目的版本号的单一真实来源

  1. 读取 setup.py 中的文件并获取版本。示例(来自 pip setup.py

    import codecs
    import os.path
    
    def read(rel_path):
        here = os.path.abspath(os.path.dirname(__file__))
        with codecs.open(os.path.join(here, rel_path), 'r') as fp:
            return fp.read()
    
    def get_version(rel_path):
        for line in read(rel_path).splitlines():
            if line.startswith('__version__'):
                delim = '"' if '"' in line else "'"
                return line.split(delim)[1]
        else:
            raise RuntimeError("Unable to find version string.")
    
    setup(
       ...
       version=get_version("package/__init__.py")
       ...
    )
    

    注意

    从 setuptools 46.4.0 版本开始,可以通过将以下内容放入项目的 setup.cfg 文件中来实现相同的功能(用“package”替换包的导入名称)

    [metadata]
    version = attr: package.__version__
    

    从 setuptools 61.0.0 版本开始,可以在项目的 pyproject.toml 文件中动态指定版本。

    [project]
    name = "package"
    dynamic = ["version"]
    
    [tool.setuptools.dynamic]
    version = {attr = "package.__version__"}
    

    请注意,在 setup.py 的参数中不支持声明性配置指示符,包括 attr: 指令。

  2. 使用外部构建工具,该工具可以管理更新这两个位置,或提供这两个位置都可以使用的 API。

    你可以使用的一些工具,不分先后顺序,也不一定完整:bump2versionchangescommitizenzest.releaser

  3. 将值设置为项目中专用模块(例如 version.py)中的 __version__ 全局变量,然后让 setup.py 读取该值并将其 exec 为变量。

    version = {}
    with open("...sample/version.py") as fp:
        exec(fp.read(), version)
    # later on we use: version['__version__']
    

    使用此技术的示例:warehouse

  4. 将值放入一个简单的 VERSION 文本文件中,并让 setup.py 和项目代码都读取它。

    with open(os.path.join(mypackage_root_dir, 'VERSION')) as version_file:
        version = version_file.read().strip()
    

    此技术的优点是它不特定于 Python。任何工具都可以读取版本。

    警告

    使用此方法时,你必须确保 VERSION 文件包含在你所有的源和二进制分发中(例如,将 include VERSION 添加到 MANIFEST.in 中)。

  5. setup.py 中设置值,并让项目代码使用 importlib.metadata API 在运行时获取值。(importlib.metadata 在 Python 3.8 中引入,并作为 importlib-metadata 项目提供给较旧版本。)已安装项目的版本可以使用以下 API 获取

    import sys
    
    if sys.version_info >= (3, 8):
        from importlib import metadata
    else:
        import importlib_metadata as metadata
    
    assert metadata.version('pip') == '1.2.0'
    

    请注意,importlib.metadata API 仅了解安装元数据中的内容,这并不一定是当前导入的代码。

    如果项目使用此方法在运行时获取其版本,那么需要编辑其 install_requires 值以在 Python 的 3.8 之前的版本上安装 importlib-metadata,如下所示

    setup(
        ...
        install_requires=[
            ...
            'importlib-metadata >= 1.0 ; python_version < "3.8"',
            ...
        ],
        ...
    )
    

    importlib.metadata 的较旧(且效率较低)的替代方法是 setuptools 提供的 pkg_resources API

    import pkg_resources
    assert pkg_resources.get_distribution('pip').version == '1.2.0'
    

    如果项目在运行时使用 pkg_resources 获取其自己的版本,则必须将 setuptools 添加到项目的 install_requires 列表中。

    使用此技术的示例:setuptools

  6. 将值设置为 __version__sample/__init__.py 中,并在 setup.py 中导入 sample

    import sample
    setup(
        ...
        version=sample.__version__
        ...
    )
    

    警告

    虽然此技术很常见,但请注意,如果 sample/__init__.pyinstall_requires 依赖项导入包,则此技术将会失败,因为在运行 setup.py 时很可能尚未安装这些依赖项。

  7. 将版本号保留在版本控制系统(Git、Mercurial 等)的标记中,而不是保留在代码中,并使用 setuptools_scm 从那里自动提取版本号。