如何获取软件包中setup.py(setuptools)中定义的版本?


Answers:


246

询问已安装发行版的版本字符串

要在运行时从程序包内部检索版本(您的问题实际上是在问什么),可以使用:

import pkg_resources  # part of setuptools
version = pkg_resources.require("MyProject")[0].version

存储版本字符串以在安装期间使用

如果要沿另一方向走动(这似乎是其他答案作者似乎认为您正在询问的问题),请将版本字符串放在单独的文件中,然后在中读取该文件的内容setup.py

您可以在包中用__version__一行创建一个version.py ,然后使用来从setup.py读取它execfile('mypackage/version.py'),以便__version__在setup.py命名空间中进行设置。

如果您想要一种更简单的方法来适用于所有Python版本,甚至可能需要访问版本字符串的非Python语言,请执行以下操作:

将版本字符串存储为纯文本文件(例如)的唯一内容VERSION,并在期间读取该文件setup.py

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

这样,同一个VERSION文件在任何其他程序中都可以很好地工作,即使在非Python程序中也是如此,并且您只需要在一个位置更改所有程序的版本字符串即可。

安装过程中有关种族状况的警告

顺便说一句,请不要按照此处另一个答案的建议从setup.py导入软件包:它似乎对您有用(因为您已经安装了软件包的依赖项),但是会对软件包的新用户造成严重破坏,因为如果不先手动安装依赖项,他们将无法安装您的软件包。


1
这只是这里的最佳答案:最好的方法是,如果您需要setup.py从软件包中获取版本,则使用execfile。
2012年

2
execfile作品真的很好......但(可惜)不使用Python 3下工作
medmunds

9
...确定,适用于Python 2 3 with open('mypackage/version.py') as f: exec(f.read())代替execfile('mypackage/version.py')。(来自stackoverflow.com/a/437857/647002
medmunds

5
严重破坏部分不一定是正确的……仅当软件包中的init .py文件具有代码导入依赖项(直接或间接)时,才会造成严重破坏。
Pykler 2013年

2
值得一提的是django 在setup.py中执行了__import__调用。'__import__'与'import'是否安全?它似乎没有给他们造成任何问题。
user1978019 2015年

33

示例研究: mymodule

想象一下这个配置:

setup.py
mymodule/
        / __init__.py
        / version.py
        / myclasses.py

然后,想象一下一些具有依赖项并且setup.py看起来像这样的常见情况:

setup(...
    install_requires=['dep1','dep2', ...]
    ...)

还有一个例子__init__.py

from mymodule.myclasses import *
from mymodule.version import __version__

例如myclasses.py

# these are not installed on your system.
# importing mymodule.myclasses would give ImportError
import dep1
import dep2

问题1:mymodule在安装过程中导入

如果您是setup.py进口公司,mymodule在设置过程中很可能会收到ImportError。当您的软件包具有依赖项时,这是一个非常常见的错误。如果您的程序包除内置程序外没有其他依赖项,则可能是安全的。但是,这不是一个好习惯。这样做的原因是它不是面向未来的。说明天您的代码需要使用其他依赖项。

问题2:我的位置在哪里__version__

如果您进行硬编码__version__setup.py则它可能与您模块中附带的版本不匹配。为了保持一致,您可以将其放在一个地方,并在需要时从同一地方阅读。使用import您可能会遇到问题1。

解决方案:àla setuptools

您可以结合使用openexec并提供dict exec来添加变量:

# setup.py
from setuptools import setup, find_packages
from distutils.util import convert_path

main_ns = {}
ver_path = convert_path('mymodule/version.py')
with open(ver_path) as ver_file:
    exec(ver_file.read(), main_ns)

setup(...,
    version=main_ns['__version__'],
    ...)

mymodule/version.py公开版本:

__version__ = 'some.semantic.version'

这样,该版本将随模块一起提供,并且在安装期间尝试导入缺少依赖项(尚未安装)的模块时不会出现问题。


2
这似乎是最合理的解决方案,因为它仅依赖于允许的依赖项。
Dogweather

在从setup.py文件不同定义的包版本的概念版本
变量

16

最好的技术是__version__在产品代码中定义,然后从那里将其导入setup.py。这为您提供了一个值,您可以在正在运行的模块中读取该值,并且只有一个地方可以对其进行定义。

setup.py中的值未安装,并且setup.py在安装后不会停留。

我在coverage.py中所做的(例如):

# coverage/__init__.py
__version__ = "3.2"


# setup.py
from coverage import __version__

setup(
    name = 'coverage',
    version = __version__,
    ...
    )

更新(2017):coverage.py不再自行导入以获取版本。导入自己的代码可以使其可卸载,因为您的产品代码将尝试导入尚未安装的依赖项,因为setup.py是安装它们的原因。


4
@Evan:我不确定“仅从源头获取该值”会带来什么好处。如果其中的文件以__version__某种方式损坏,则导入也将损坏。Python不知道如何仅解释所需的语句。@pjeby是正确的:如果您的模块需要导入其他模块,则可能尚未安装它们,这将会很混乱。如果您谨慎地确保导入不会引起其他导入的长链影响,则可以使用此技术。
Ned Batchelder

1
@Ned Batchelder我很确定,如果您将版本放在源文件中的任何导入之前,并且仅放置到“从模块导入版本”中,它将不会对源文件进行过多的处理。此外,谁将发布损坏的代码?如果软件包需要依赖项,请使用setuptools或等待今年晚些时候发布distutils2。
Evan Plaice 2010年

15
@Evan,对不起,但是您导入部分文件是错误的。尝试将打印语句放在长模块的末尾,然后导入文件中定义的第一个变量。打印语句将执行。
Ned Batchelder

23
我对此表示赞同,但@pjeby完全正确:“顺便说一句,请勿按照此处另一个答案的建议从setup.py导入软件包:它似乎对您有用(因为您已经安装了软件包的依赖项),但会对软件包的新用户造成严重破坏,因为如果不先手动安装依赖项,他们将无法安装软件包。” 内德,您介意在警告中添加警告吗?
Jan-Philip Gehrcke博士

1
像@ Jan-PhilipGehrcke一样,当我将类似setup.py文件上传到PyPI时,pip install <packagename>出现以下错误:ImportError: No module named <packagename>。请警告您的读者,您不能在尚未安装该软件包的环境中运行此类的setup.py文件!
滚刀

14

您的问题有点模糊,但是我想您要问的是如何指定它。

您需要这样定义__version__

__version__ = '1.4.4'

然后,您可以确认setup.py知道您刚刚指定的版本:

% ./setup.py --version
1.4.4

9

我对这些答案不满意...不想使用setuptools,也不想为单个变量制作整个单独的模块,所以我想到了这些。

当您确定主模块为pep8样式并将保持这种状态时:

version = '0.30.unknown'
with file('mypkg/mymod.py') as f:
    for line in f:
        if line.startswith('__version__'):
            _, _, version = line.replace("'", '').split()
            break

如果您要格外小心并使用真实的解析器:

import ast
version = '0.30.unknown2'
with file('mypkg/mymod.py') as f:
    for line in f:
        if line.startswith('__version__'):
            version = ast.parse(line).body[0].value.s
            break

setup.py在某种程度上是一个一次性模块,因此如果它有点难看,则不是问题。


更新:有趣的是,近年来,我已经摆脱了这一点,开始使用名为的软件包中的单独文件meta.py。我在其中放置了很多可能需要经常更改的元数据。因此,不仅仅是为了一个价值。


+1。它相对简单,将版本号保存在一个地方,不需要单独的文件来包含它,也没有将python模块的导入依赖项强加于setup.py。在一个像setup.py这样短暂的程序中,我什至不会为上下文管理器而烦恼。
ʇsәɹoɈ

很多更好的比使用正则表达式,谢谢。您也可以将其ast.get_docstring()与其他内容.split('\n')[0].strip()一起使用,以自动description从源代码中进行填写。要保持同步的另一件事

4

在源代码树中创建文件,例如在yourbasedir / yourpackage / _version.py中。让该文件仅包含一行代码,如下所示:

__version__ = "1.1.0-r4704"

然后在setup.py中,打开该文件并解析出如下版本号:

verstr =“未知”
尝试:
    verstrline = open('yourpackage / _version.py',“ rt”)。read()
除了EnvironmentError:
    通过#好的,没有版本文件。
其他:
    VSRE = r“ ^ __ version__ = ['\”]([^ \\]] *)['\“]”
    mo = re.search(VSRE,verstrline,re.M)
    如果莫:
        verstr = mo.group(1)
    其他:
        引发RuntimeError(“无法在您的package / _version.py中找到版本”)

最后,在yourbasedir/yourpackage/__init__.pyimport _version中,如下所示:

__version__ =“未知”
尝试:
    从_version导入__version__
除了ImportError:
    #我们正在没有_version.py的树中运行,因此我们不知道我们的版本是什么。
    通过

我维护的“ pyutil”包就是一个执行此操作的代码示例。(请参阅PyPI或Google搜索-stackoverflow不允许我在此答案中包含指向它的超链接。)

@pjeby是正确的,您不应从其自己的setup.py导入软件包。当您通过创建一个新的Python解释器并在其中首先执行setup.py对其python setup.py进行测试时,它将起作用,但是在某些情况下它将无法工作。那是因为import youpackage这并不意味着要读取名为“ yourpackage”的目录的当前工作目录,而是要在当前目录中查找sys.modules键“ yourpackage”,然后在不存在的情况下执行各种操作。所以它总是在工作时起作用,python setup.py因为您有一个空的sys.modules,但这通常并不起作用。

例如,如果py2exe在打包应用程序的过程中正在执行setup.py怎么办?我见过这样的情况,其中py2exe会在软件包上放置错误的版本号,因为该软件包正在从中获取其版本号import myownthing在其setup.py中,但是以前在py2exe运行期间导入了该软件包的其他版本。同样,如果setuptools,easy_install,distribution或distutils2尝试在安装依赖于您的软件包的其他软件包的过程中尝试构建软件包,该怎么办?然后,在评估setup.py时是否可以导入您的软件包,或者在此Python解释器的生命周期中是否已经导入了您的软件包的版本,或者导入您的软件包是否需要先安装其他软件包? ,或有副作用,可以改变结果。我在尝试重新使用Python软件包时遇到了很多困难,这导致py2exe和setuptools之类的工具出现问题,因为它们的setup.py会导入软件包本身以查找其版本号。

顺便说一句,这种技术可以很好地与自动yourpackage/_version.py为您创建文件的工具配合使用,例如,通过读取修订控制历史记录并根据修订控制历史记录中的最新标记写出版本号来实现。这是一个针对darcs的工具:http : //tahoe-lafs.org/trac/darcsver/browser/trunk/README.rst,这是一个对git进行相同操作的代码片段:http:// github .com / warner / python-ecdsa / blob / 0ed702a9d4057ecf33eea969b8cf280eaccd89a1 / setup.py#L34


3

使用正则表达式并根据元数据字段具有如下格式,这也应该起作用:

__fieldname__ = 'value'

在setup.py的开头使用以下命令:

import re
main_py = open('yourmodule.py').read()
metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", main_py))

之后,您可以像下面这样在脚本中使用元数据:

print 'Author is:', metadata['author']
print 'Version is:', metadata['version']

正如我前面所说,只要使用str.split :)
埃里克·阿劳霍

天哪 您正在使用Python解析Python吗?至少要使用eval()(子级)。
slacy 2012年

3
咨询不便,应尽量避免评估。
Gringo Suave 2012年

3

具有这样的结构:

setup.py
mymodule/
        / __init__.py
        / version.py
        / myclasses.py

其中version.py包含:

__version__ = 'version_string'

您可以在setup.py中执行此操作:

import sys

sys.path[0:0] = ['mymodule']

from version import __version__

这不会对mymodule / __ init__.py中的任何依赖项造成任何问题


2

为了避免导入文件(从而执行其代码),可以解析文件并version从语法树中恢复属性:

# assuming 'path' holds the path to the file

import ast

with open(path, 'rU') as file:
    t = compile(file.read(), path, 'exec', ast.PyCF_ONLY_AST)
    for node in (n for n in t.body if isinstance(n, ast.Assign)):
        if len(node.targets) == 1:
            name = node.targets[0]
            if isinstance(name, ast.Name) and \
                    name.id in ('__version__', '__version_info__', 'VERSION'):
                v = node.value
                if isinstance(v, ast.Str):
                    version = v.s
                    break
                if isinstance(v, ast.Tuple):
                    r = []
                    for e in v.elts:
                        if isinstance(e, ast.Str):
                            r.append(e.s)
                        elif isinstance(e, ast.Num):
                            r.append(str(e.n))
                    version = '.'.join(r)
                    break

这段代码试图在模块的顶层查找__version__VERSION返回字符串值。右侧可以是字符串或元组。


3
这看起来很聪明和复杂:)拥有一个包含一个分配的_version.py文件并使用open-read-str.split进行解析会更简单。
埃里克·阿劳霍

感谢您发布此信息。我认为这种情况太过复杂了,但是对于理解人们如何解决该问题非常有帮助。我喜欢它将版本号保存在一个地方,不需要单独的文件来包含它,也没有将python模块的导入依赖项强加于安装脚本。
ʇsәɹoɈ

2

有上千种为猫皮的方法-这是我的:

# Copied from (and hacked):
# https://github.com/pypa/virtualenv/blob/develop/setup.py#L42
def get_version(filename):
    import os
    import re

    here = os.path.dirname(os.path.abspath(__file__))
    f = open(os.path.join(here, filename))
    version_file = f.read()
    f.close()
    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
                              version_file, re.M)
    if version_match:
        return version_match.group(1)
    raise RuntimeError("Unable to find version string.")

2

从@ gringo-suave 清理https://stackoverflow.com/a/12413800

from itertools import ifilter
from os import path
from ast import parse

with open(path.join('package_name', '__init__.py')) as f:
    __version__ = parse(next(ifilter(lambda line: line.startswith('__version__'),
                                     f))).body[0].value.s

2

现在这很麻烦,需要进行一些改进(我可能错过了pkg_resources中甚至可能有一个未发现的成员调用),但是我根本看不出为什么这行不通,也为什么至今没有人建议这样做(在Google周围搜索没有出现)...请注意,这是Python 2.x,需要pkg_resources(叹气):

import pkg_resources

version_string = None
try:
    if pkg_resources.working_set is not None:
        disto_obj = pkg_resources.working_set.by_key.get('<my pkg name>', None)
        # (I like adding ", None" to gets)
        if disto_obj is not None:
            version_string = disto_obj.version
except Exception:
    # Do something
    pass

2

我们希望将有关我们程序包的元信息放入pypackagery__init__.py,但由于PJ Eby已经指出(请参阅他的回答和有关比赛条件的警告),它具有第三方依赖性,因此无法这样做。

我们通过创建一个pypackagery_meta.py包含元信息的单独模块来解决该问题:

"""Define meta information about pypackagery package."""

__title__ = 'pypackagery'
__description__ = ('Package a subset of a monorepo and '
                   'determine the dependent packages.')
__url__ = 'https://github.com/Parquery/pypackagery'
__version__ = '1.0.0'
__author__ = 'Marko Ristin'
__author_email__ = 'marko.ristin@gmail.com'
__license__ = 'MIT'
__copyright__ = 'Copyright 2018 Parquery AG'

然后将元信息导入packagery/__init__.py

# ...

from pypackagery_meta import __title__, __description__, __url__, \
    __version__, __author__, __author_email__, \
    __license__, __copyright__

# ...

并最终用于setup.py

import pypackagery_meta

setup(
    name=pypackagery_meta.__title__,
    version=pypackagery_meta.__version__,
    description=pypackagery_meta.__description__,
    long_description=long_description,
    url=pypackagery_meta.__url__,
    author=pypackagery_meta.__author__,
    author_email=pypackagery_meta.__author_email__,
    # ...
    py_modules=['packagery', 'pypackagery_meta'],
 )

您必须pypackagery_meta使用py_modulessetup参数将其包含在包中。否则,您将无法在安装时导入它,因为打包的发行版将缺少它。


1

简单明了,创建一个source/package_name/version.py包含以下内容的文件:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
__version__ = "2.6.9"

然后,在文件上source/package_name/__init__.py,导入供其他人使用的版本:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from .version import __version__

现在,您可以将其放在 setup.py

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re
import sys

try:
    filepath = 'source/package_name/version.py'
    version_file = open( filepath )
    __version__ ,= re.findall( '__version__ = "(.*)"', version_file.read() )

except Exception as error:
    __version__ = "0.0.1"
    sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )

finally:
    version_file.close()

与Python测试了这个2.73.33.43.53.63.7在Linux,Windows和Mac OS。我在包装上使用了针对所有这些平台的集成和单元测试。您可以从.travis.ymlappveyor.yml在此处查看结果:

  1. https://travis-ci.org/evandrocoan/debugtools/builds/527110800
  2. https://ci.appveyor.com/project/evandrocoan/pythondebugtools/builds/24245446

备用版本使用上下文管理器:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re
import sys

try:
    filepath = 'source/package_name/version.py'

    with open( filepath ) as file:
        __version__ ,= re.findall( '__version__ = "(.*)"', file.read() )

except Exception as error:
    __version__ = "0.0.1"
    sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )

您还可以使用该codecs模块在Python 2.73.6

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re
import sys
import codecs

try:
    filepath = 'source/package_name/version.py'

    with codecs.open( filepath, 'r', errors='ignore' ) as file:
        __version__ ,= re.findall( '__version__ = "(.*)"', file.read() )

except Exception as error:
    __version__ = "0.0.1"
    sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )

如果要使用Python C扩展在C / C ++中100%编写Python模块,则可以执行相同的操作,但要使用C / C ++而不是Python。

在这种情况下,创建以下内容setup.py

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re
import sys
import codecs
from setuptools import setup, Extension

try:
    filepath = 'source/version.h'

    with codecs.open( filepath, 'r', errors='ignore' ) as file:
        __version__ ,= re.findall( '__version__ = "(.*)"', file.read() )

except Exception as error:
    __version__ = "0.0.1"
    sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )

setup(
        name = 'package_name',
        version = __version__,

        package_data = {
                '': [ '**.txt', '**.md', '**.py', '**.h', '**.hpp', '**.c', '**.cpp' ],
            },

        ext_modules = [
            Extension(
                name = 'package_name',
                sources = [
                    'source/file.cpp',
                ],
                include_dirs = ['source'],
            )
        ],
    )

从文件中读取版本version.h

const char* __version__ = "1.0.12";

但是,不要忘记创建MANIFEST.in包含version.h文件:

include README.md
include LICENSE.txt

recursive-include source *.h

并通过以下方式集成到主要应用程序中:

#include <Python.h>
#include "version.h"

// create the module
PyMODINIT_FUNC PyInit_package_name(void)
{
    PyObject* thismodule;
    ...

    // https://docs.python.org/3/c-api/arg.html#c.Py_BuildValue
    PyObject_SetAttrString( thismodule, "__version__", Py_BuildValue( "s", __version__ ) );

    ...
}

参考文献:

  1. python打开文件错误
  2. 通过C API在Python模块中定义全局变量
  3. 如何在setuptools / distribute中包含软件包数据?
  4. https://github.com/lark-parser/lark/blob/master/setup.py#L4
  5. 如何使用具有相同名称的setuptools软件包和ext_modules?
  6. 是否可以使用dist utils(setup.py)作为包数据的一部分包含子目录?

1

将包部署到服务器和索引包的文件命名约定:

pip动态版本转换的示例:

  • 赢得:

    • test_pkg-1.0.0-cp36-cp36m-win_amd64.whl
    • test_pkg-1.0.0-py3.6-win-amd64.egg
  • 苹果电脑:

    • test_pkg-1.0.0-py3.7-macosx-10.12-x86_64.egg
    • test_pkg-1.0.0-py3.7-macosx-10.12-x86_64.whl
  • Linux:
    • test_pkg-1.0.0-cp36-cp36m-linux_x86_64.whl
from setuptools_scm import get_version

def _get_version():

     dev_version = str(".".join(map(str, str(get_version()).split("+")[0]\
            .split('.')[:-1])))

    return dev_version

找到示例setup.py从git commit调用匹配的动态pip版本

setup(
    version=_get_version(),
    name=NAME,
    description=DESCRIPTION,
    long_description=LONG_DESCRIPTION,
    classifiers=CLASSIFIERS,

# add few more for wheel wheel package ...conversion

)

0

我正在使用以下环境变量

VERSION = 0.0.0 python setup.py sdist bdist_wheel

在setup.py中


import os

setup(
    version=os.environ['VERSION'],
    ...
)

为了与打包程序版本进行一致性检查,我使用以下脚本。

PKG_VERSION=`python -c "import pkg; print(pkg.__version__)"`
if [ $PKG_VERSION == $VERSION ]; then
    python setup.py sdist bdist_wheel
else
    echo "Package version differs from set env variable"
fi
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.