如何在我的包中获得setup.py(setuptools)中定义的版本?

时间:2010-01-13 17:24:50

标签: python setuptools

如何从我的包中获取setup.py中定义的版本(--version或其他目的)?

16 个答案:

答案 0 :(得分:220)

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

要在运行时从包内检索版本(您的问题似乎实际上是在询问),您可以使用:

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

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

如果你想转向其他方式(这似乎是其他答案作者在这里似乎认为你在问),请将版本字符串放在一个单独的文件中并在{{1}中读取该文件的内容}。

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

如果你想要一种更简单的方法,它可以用于所有Python版本,甚至可能需要访问版本字符串的非Python语言:

将版本字符串存储为纯文本文件的唯一内容,例如, __version__,并在VERSION期间阅读该文件。

setup.py

同样的version_file = open(os.path.join(mypackage_root_dir, 'VERSION')) version = version_file.read().strip() 文件在任何其他程序中都可以正常工作,甚至是非Python程序,你只需要在一个地方为所有程序更改版本字符串。

有关安装期间竞争条件的警告

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

答案 1 :(得分:29)

示例研究: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。

解决方案:àlasetuptools

您可以使用openexec的组合,并为exec提供一个dict来添加变量:

# 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 :(得分:14)

最好的方法是在产品代码中定义__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__,
    ...
    )

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

答案 3 :(得分:12)

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

您需要像这样定义__version__

__version__ = '1.4.4'

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

% ./setup.py --version
1.4.4

答案 4 :(得分:7)

我对这些答案不满意......不想要设置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有点像一次性模块,所以如果它有点难看就不是问题。

答案 5 :(得分:4)

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

__version__ = "1.1.0-r4704"

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

verstr = "unknown"
try:
    verstrline = open('yourpackage/_version.py', "rt").read()
except EnvironmentError:
    pass # Okay, there is no version file.
else:
    VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
    mo = re.search(VSRE, verstrline, re.M)
    if mo:
        verstr = mo.group(1)
    else:
        raise RuntimeError("unable to find version in yourpackage/_version.py")

最后,在yourbasedir/yourpackage/__init__.py import _version中这样:

__version__ = "unknown"
try:
    from _version import __version__
except ImportError:
    # We're running in a tree that doesn't have a _version.py, so we don't know what our version is.
    pass

执行此操作的代码示例是我维护的“pyutil”包。 (请参阅PyPI或谷歌搜索 - 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会在包上放错版本号,因为包在其setup.py中从import myownthing获得了版本号,但之前导入了该包的不同版本在py2exe运行期间。同样,如果setuptools,easy_install,distribute或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

答案 6 :(得分: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']

答案 7 :(得分: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中的任何依赖项造成任何问题

答案 8 :(得分: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赋值返回字符串值。右侧可以是字符串或元组。

答案 9 :(得分: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.")

答案 10 :(得分: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

答案 11 :(得分: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_modules包含在软件包中。否则,您将无法在安装时导入它,因为打包的发行版将缺少它。

答案 12 :(得分:1)

现在这很糟糕,需要进行一些改进(在我错过的pkg_resources中甚至可能有一个未被覆盖的成员调用),但我根本不明白为什么这不起作用,也不知道为什么没有人建议它(谷歌搜索周围没有提高这一点)...请注意,这是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

答案 13 :(得分:1)

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

pip动态版本转换的示例:

  • 赢:

    • test_pkg-1.0.0-cp36-cp36m-win_amd64.whl
    • test_pkg-1.0.0-py3.6-win-amd64.egg
  • mac:

    • 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

)

答案 14 :(得分:0)

简单明了,创建一个名为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()

在Linux,Windows和Mac OS上使用Python 2.73.33.43.53.63.7对此进行了测试。我在包装上使用了针对所有这些平台的集成和单元测试。您可以在此处查看.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上的unicode错误

#!/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 open file error
  2. Define a global in a Python module from a C API
  3. How to include package data with setuptools/distribute?
  4. https://github.com/lark-parser/lark/blob/master/setup.py#L4
  5. How to use setuptools packages and ext_modules with the same name?
  6. Is it possible to include subdirectories using dist utils (setup.py) as part of package data?

答案 15 :(得分: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