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

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

125737 次浏览

你的问题有点含糊,但我认为你问的是如何具体说明。

你需要这样定义 __version__:

__version__ = '1.4.4'

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

% ./setup.py --version
1.4.4

最好的技术是在产品代码中定义 __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。

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

要在运行时从包中检索版本(您的问题看起来实际上是在问这个问题) ,您可以使用:

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

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

如果你想反过来(这似乎是其他答案的作者似乎认为你在问) ,把版本字符串放在一个单独的文件,并读取该文件的内容在 setup.py

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

安装过程中关于竞态状态的警告

顺便说一下,不要从 setup.py 中导入软件包,这里有另一个答案: 它似乎对你有用(因为你已经安装了软件包的依赖项) ,但是它会对你软件包的新用户造成严重破坏,因为他们如果不首先手动安装依赖项就无法安装你的软件包。

为了避免导入文件(从而执行其代码) ,可以解析它并从语法树中恢复 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赋值。右边可以是字符串或元组。

在源代码树中创建一个文件,例如在 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 或 google search —— 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、 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

使用正则表达式并依赖于元数据字段,这样的格式也应该有效:

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

我对这些答案不满意... ... 不想要 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

Py 在某种程度上是一个一次性模块,所以如果它有点难看也不是问题。


更新: 有趣的是,最近几年我已经不再使用这种方法,而是开始在包中使用一个名为 meta.py的单独文件。我在里面放了很多元数据,我可能会经常更改它们。所以,不只是为了一个值。

剥猫皮的方法有一千种,这是我的:

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

示例研究: 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__呢?

如果您在 setup.py中硬编码 __version__,那么它可能与您在模块中提供的版本不匹配。为了保持一致,你可以把它放在一个地方,当你需要的时候从同一个地方读出来。使用 import,您可能会遇到问题 # 1。

解决方案: 从 setuptools开始

你可以使用 openexec的组合,并为 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'

通过这种方式,版本随模块一起发布,并且在安装过程中尝试导入缺少依赖项(尚未安装)的模块时不会出现问题。

从@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

现在这很恶心,需要一些改进(甚至可能在 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

我们想把我们的软件包 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'],
)

您必须使用 py_modules设置参数将 pypackagery_meta包含到包中。否则,您不能在安装时导入它,因为打包的发行版将缺少它。

像这样的结构:

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 中的任何依赖性造成任何问题

简单直接,创建一个名为 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 操作系统上用 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模块在 Python2.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 扩展100% 地使用 C/C + + 编写 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";

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

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. 译自: 美国《科学》杂志网站(http://stackoverflow. com/questions/30700166/python-open-file-error)原文地址: http://stackoverflow. com/questions/30700166/python-open-file-error
  2. 在 C API 中定义 Python 模块中的全局变量
  3. 如何将包数据包含在 setuptools/distribution 中
  4. https://github.com/lark-parser/lark/blob/master/setup.py#L4
  5. 如何使用名称相同的 setuptools 包和 ext _ module
  6. 是否有可能包含使用 dist utils (setup.py)作为包数据一部分的子目录

将软件包部署到服务器和索引软件包的文件变数命名原则:

Pip 动态版本转换示例:

  • 胜利:

    • Test _ pkg-1.0.0-cp36-cp36m-win _ amd64.whl
    • Test _ pkg-1.0.0-py3.6-win-amd64. egg
  • < p > 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
  • < li > 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 提交匹配的动态 pip 版本

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


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


)

我使用的环境变量如下

VERSION = 0.0.0 python setup.py sdist bdist _ eler

在 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

值得一提的是,我编写 获得版本是为了解决我们的一个项目需要的这个问题。它依赖于一系列符合 PEP 的策略来返回模块的版本,并为开发模式(git scm)添加一些策略。

例如:

from getversion import get_module_version


# Get the version of an imported module
from xml import dom
version, details = get_module_version(dom)
print(version)

投降

3.7.3.final.0

为什么会发现这个版本? 你可以从 details中了解到:

> print(details)
Version '3.7.3.final.0' found for module 'xml.dom' by strategy 'get_builtin_module_version', after the following failed attempts:
- Attempts for module 'xml.dom':
- <get_module_version_attr>: module 'xml.dom' has no attribute '__version__'
- Attempts for module 'xml':
- <get_module_version_attr>: module 'xml' has no attribute '__version__'
- <get_version_using_pkgresources>: Invalid version number: None
- <get_builtin_module_version>: SUCCESS: 3.7.3.final.0

更多信息可以在 文件中找到。

我创建了正则表达式模式来查找 setup.cfg ?:[\s]+|[\s])?[=](?:[\s]+|[\s])?(.*)中的版本号

import re
with open("setup.cfg", "r") as _file:
data = _file.read()
print(re.findall(r"\nversion(?:[\s]+|[\s])?[=](?:[\s]+|[\s])?(.*)", data))
# -> ['1.1.0']

很多其他的答案都已经过时了,我相信从已安装的 python 3.10包获取版本信息的标准方法是使用 import lib.meta 作为 PEP-0566

官方文件: https://docs.python.org/3.10/library/importlib.metadata.html

from importlib.metadata import version
VERSION_NUM = version("InstalledPackageName")

这是简单,干净,没有大惊小怪。

如果你在安装包时运行的脚本中做了一些奇怪的事情,这是不会起作用的,但是如果你所做的只是通过 CLI —— help 命令,about box,或者其他任何你的包已经安装并且只需要安装的版本号的地方获取版本号,那么这对我来说似乎是最好的解决方案。