【发布时间】:2011-01-04 18:04:58
【问题描述】:
如何从我的包中获取setup.py 中定义的版本(用于--version,或其他目的)?
【问题讨论】:
-
我认为这与stackoverflow.com/questions/458550/…的问题大致相同。
标签: python setuptools
如何从我的包中获取setup.py 中定义的版本(用于--version,或其他目的)?
【问题讨论】:
标签: python setuptools
要在运行时从包中检索版本(您的问题实际上是在问什么),您可以使用:
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 导入您的包:它似乎对您有用(因为您已经安装了包的依赖项),但它会对新用户造成严重破坏您的软件包,因为如果不先手动安装依赖项,他们将无法安装您的软件包。
【讨论】:
execfile 工作得非常好......但(遗憾的是)不适用于 Python 3。
with open('mypackage/version.py') as f: exec(f.read()) 代替 execfile('mypackage/version.py')。 (来自stackoverflow.com/a/437857/647002)
__version__ = importlib.metadata.version('Example')docs.python.org/3.8/library/importlib.metadata.html
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
mymodule
如果您的setup.py 导入mymodule,那么在设置期间您很可能会得到ImportError。当您的包具有依赖项时,这是一个非常常见的错误。如果你的包没有除了内置的其他依赖,你可能是安全的;但这不是一个好习惯。原因是它不是面向未来的。说明天你的代码需要消耗一些其他的依赖。
__version__ 呢?如果您在setup.py 中硬编码__version__,那么它可能与您在模块中发布的版本不匹配。为了保持一致,您可以将它放在一个地方,并在需要时从同一个地方读取它。使用import 可能会遇到问题#1。
setuptools
您将使用open、exec 的组合并为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'
这样,版本随模块一起提供,并且在安装过程中尝试导入缺少依赖项(尚未安装)的模块时不会遇到问题。
【讨论】:
最好的方法是在您的产品代码中定义__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 是安装它们的。
【讨论】:
__version__ 的文件以某种方式损坏,那么您的导入也将被损坏。 Python 不知道如何只解释您想要的语句。 @pjeby 是对的:如果您的模块需要导入其他模块,它们可能尚未安装,并且会很混乱。如果您注意导入不会导致其他导入的长链,则此技术有效。
pip install <packagename> 时,就像 @Jan-PhilipGehrcke 一样,我收到以下错误:ImportError: No module named <packagename>。请警告您的读者,您不能在尚未安装软件包的环境中运行这样的 setup.py 文件!
您的问题有点模糊,但我认为您要问的是如何指定它。
你需要像这样定义__version__:
__version__ = '1.4.4'
然后您可以确认 setup.py 知道您刚刚指定的版本:
% ./setup.py --version
1.4.4
【讨论】:
我对这些答案不满意...不想要求安装工具,也不想为单个变量创建一个完整的单独模块,所以我想出了这些。
当你确定主模块是 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 的单独文件。我在那里放了很多我可能想要经常更改的元数据。所以,不仅仅是一个价值。
【讨论】:
ast.get_docstring() 和一些.split('\n')[0].strip() 等自动从源填写description。少一件事要保持同步
结构如下:
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 中的任何依赖项造成任何问题
【讨论】:
在源代码树中创建一个文件,例如在 yourbasedir/yourpackage/_version.py 。让该文件只包含一行代码,如下所示:
__version__ = "1.1.0-r4704"
然后在 setup.py 中,打开该文件并解析出版本号,如下所示:
verstr = "未知" 尝试: verstrline = open('yourpackage/_version.py', "rt").read() 除了环境错误: pass # 好了,没有版本文件。 别的: VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" mo = re.search(VSRE, verstrline, re.M) 如果莫: verstr = mo.group(1) 别的: raise RuntimeError("unable to find version in yourpackage/_version.py")最后,在yourbasedir/yourpackage/__init__.py import _version 像这样:
执行此操作的代码示例是我维护的“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,这是一个代码 sn-p,它对 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']
【讨论】:
我们想将关于我们的包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 包含到您的包中。否则,您无法在安装时导入它,因为打包发行版会缺少它。
【讨论】:
为避免导入文件(从而执行其代码),可以对其进行解析并从语法树中恢复 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 赋值返回字符串值。右边可以是字符串也可以是元组。
【讨论】:
给猫剥皮有一千种方法——这是我的:
# 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.")
【讨论】:
从@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
【讨论】:
简单直接,创建一个名为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.7、3.3、3.4、3.5、3.6 和 3.7 对此进行了测试。我在我的包上使用了所有这些平台的集成和单元测试。你可以在这里看到.travis.yml和appveyor.yml的结果:
另一个版本正在使用上下文管理器:
#!/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.7 和 3.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";
但是,不要忘记创建 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__ ) );
...
}
参考资料:
【讨论】:
将包部署到服务器和索引包的文件命名约定:
pip动态版本转换示例:
赢:
mac:
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
)
【讨论】:
我正在使用如下环境变量
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
【讨论】: