与 setup.py 和包共享包版本的正确方法是什么?

对于 distutilssetuptools等,在 setup.py中指定了一个软件包版本:

# file: setup.py
...
setup(
name='foobar',
version='1.0.0',
# other attributes
)

我希望能够访问相同的版本号从软件包:

>>> import foobar
>>> foobar.__version__
'1.0.0'

我可以将 __version__ = '1.0.0'添加到包的 _ _ init _ _ 中。Py,但是我还想在包中包含额外的导入,以创建一个到包的简化界面:

# file: __init__.py


from foobar import foo
from foobar.bar import Bar


__version__ = '1.0.0'

还有

# file: setup.py


from foobar import __version__
...
setup(
name='foobar',
version=__version__,
# other attributes
)

但是,如果导入了其他尚未安装的包,这些额外的导入会导致 foobar安装失败。与 setup.py 和包共享包版本的正确方法是什么?

24417 次浏览

我不认为这个问题有一个标准的答案,但是我的方法(或者直接复制,或者从我在其他地方看到的方法中稍微调整)如下:

文件夹层次结构(仅限相关文件) :

package_root/
|- main_package/
|   |- __init__.py
|   `- _version.py
`- setup.py

返回文章页面

"""Version information."""


# The following line *must* be the last in the module, exactly as formatted:
__version__ = "1.0.0"

返回文章页面

"""Something nice and descriptive."""


from main_package.some_module import some_function_or_class
# ... etc.
from main_package._version import __version__


__all__ = (
some_function_or_class,
# ... etc.
)

返回文章页面

from setuptools import setup


setup(
version=open("main_package/_version.py").readlines()[-1].split()[-1].strip("\"'"),
# ... etc.
)

... 丑陋得像罪恶一样... 但是它确实有效,我见过它或者类似的东西,由那些我认为知道更好方法的人们分发的包裹里。

仅在 setup.py中设置版本,并使用 pkg_resources读取您自己的版本,有效地查询 setuptools元数据:

档案: setup.py

setup(
name='foobar',
version='1.0.0',
# other attributes
)

档案: __init__.py

from pkg_resources import get_distribution


__version__ = get_distribution('foobar').version

为了在所有情况下都能正常工作,您可以在没有安装它的情况下运行它,测试 DistributionNotFound和分发位置:

from pkg_resources import get_distribution, DistributionNotFound
import os.path


try:
_dist = get_distribution('foobar')
# Normalize case for Windows systems
dist_loc = os.path.normcase(_dist.location)
here = os.path.normcase(__file__)
if not here.startswith(os.path.join(dist_loc, 'foobar')):
# not installed, but there is another version that *is*
raise DistributionNotFound
except DistributionNotFound:
__version__ = 'Please install this project with setup.py'
else:
__version__ = _dist.version

基于 接受的答案和评论,以下是我最后所做的:

档案: setup.py

setup(
name='foobar',
version='1.0.0',
# other attributes
)

档案: __init__.py

from pkg_resources import get_distribution, DistributionNotFound


__project__ = 'foobar'
__version__ = None  # required for initial installation


try:
__version__ = get_distribution(__project__).version
except DistributionNotFound:
VERSION = __project__ + '-' + '(local)'
else:
VERSION = __project__ + '-' + __version__
from foobar import foo
from foobar.bar import Bar

说明:

  • __project__是要安装的项目的名称,它可能是 与包的名称

  • 不同
  • VERSION是我在命令行界面中显示的 请求 --version

  • 额外的导入(只适用于简化的包接口) 如果项目实际上已经安装

  • ,则会发生

我同意 @ stefano-m 的哲学关于:

在源代码中使用 版本 = “ x.y.z”并在其中解析它 Py 绝对是正确的解决方案,恕我直言 (反过来)依赖于运行时的魔力。

这个答案来自于“ Zero-Piraeus 的 回答。关键是“不要在 setup.py 中使用导入,而是从文件中读取版本”。

我使用正则表达式解析 __version__,因此它根本不需要成为专用文件的最后一行。事实上,我仍然将单源真理 __version__放在我的项目的 __init__.py中。

文件夹层次结构(仅限相关文件) :

package_root/
|- main_package/
|   `- __init__.py
`- setup.py

返回文章页面

# You can have other dependency if you really need to
from main_package.some_module import some_function_or_class


# Define your version number in the way you mother told you,
# which is so straightforward that even your grandma will understand.
__version__ = "1.2.3"


__all__ = (
some_function_or_class,
# ... etc.
)

返回文章页面

from setuptools import setup
import re, io


__version__ = re.search(
r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]',  # It excludes inline comment too
io.open('main_package/__init__.py', encoding='utf_8_sig').read()
).group(1)
# The beautiful part is, I don't even need to check exceptions here.
# If something messes up, let the build process fail noisy, BEFORE my release!


setup(
version=__version__,
# ... etc.
)

虽然还不是很理想,但是很有效。

顺便说一下,现在你可以用这种方式测试你的新玩具:

python setup.py --version
1.2.3

PS: 这个 Python 正式打包文档(及其 镜子)描述了更多的选项。它的第一个选项也是使用正则表达式。(取决于您使用的正则表达式的确切位置,它可以处理版本字符串中的引号,也可以不处理引号。不过一般来说不是什么大问题。)

PPS: 在 ADAL Python 中修复现在被反向移植到这个答案中。

__version__放在 your_pkg/__init__.py中,并使用 astsetup.py中进行解析:

import ast
import importlib.util


from pkg_resources import safe_name


PKG_DIR = 'my_pkg'


def find_version():
"""Return value of __version__.


Reference: https://stackoverflow.com/a/42269185/
"""
file_path = importlib.util.find_spec(PKG_DIR).origin
with open(file_path) as file_obj:
root_node = ast.parse(file_obj.read())
for node in ast.walk(root_node):
if isinstance(node, ast.Assign):
if len(node.targets) == 1 and node.targets[0].id == "__version__":
return node.value.s
raise RuntimeError("Unable to find version string.")


setup(name=safe_name(PKG_DIR),
version=find_version(),
packages=[PKG_DIR],
...
)

如果使用 Python < 3.4,请注意 importlib.util.find_spec不可用。此外,当然不能指望 importlib的任何后端对 setup.py可用。在这种情况下,使用:

import os


file_path = os.path.join(os.path.dirname(__file__), PKG_DIR, '__init__.py')

接受的答案要求已安装包。在我的例子中,我需要从源 setup.py中提取安装参数(包括 __version__)。我发现了一个直接和简单的解决方案,同时通过看 Setuptools 包的测试。寻找更多关于 _setup_stop_after属性的信息导致我到 一个旧的邮件列表邮件它提到了 distutils.core.run_setup,这导致我到 真正需要的文件。在所有这些之后,这里有一个简单的解决方案:

文件 setup.py:

from setuptools import setup


setup(name='funniest',
version='0.1',
description='The funniest joke in the world',
url='http://github.com/storborg/funniest',
author='Flying Circus',
author_email='flyingcircus@example.com',
license='MIT',
packages=['funniest'],
zip_safe=False)

文件 extract.py:

from distutils.core import run_setup
dist = run_setup('./setup.py', stop_after='init')
dist.get_version()

很晚了,我知道,但这对我有用。

Module/version. py:

__version__ = "1.0.2"


if __name__ == "__main__":
print(__version__)

Module/_ _ init _ _. py:

from . import version
__version__ = version.__version__

Setup.py:

import subprocess


out = subprocess.Popen(['python', 'module/version.py'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
stdout,stderr = out.communicate()
version = str(stdout)

对我来说,主要的优点是它不需要手工编写的解析或正则表达式,或者在条目中声明。它也是相当 Python 的,似乎在所有情况下都可以工作(pip-e 等) ,并且可以通过在 version. py 中使用 argparse 轻松地扩展以共享 docstring 等。有人能看出这种方法的问题吗?

Setuptools 46.4.0添加了基本的抽象语法树分析支持,这样 指令就不必导入软件包的依赖项。这使得有可能有一个单一的真理来源的软件包版本,从而过时的许多解决方案在以前的答案发布之前 setupstool46.4.0。

现在可以避免在 setup.py 中将 version 传递给 setuptools.setup 函数,如果 _ _ version _ _ 在 yourpackage 中初始化的话。(咒语)。Py 和以下元数据被添加到包的 setup.cfg 文件中。使用这个配置,setuptools.setup 函数将自动解析来自 yourpackage 的包版本。(咒语)。Py,您可以自由地导入 _ _ version _ _。在应用程序中按需填写。

例子

Py ,但没有将版本传递给安装程序

from setuptools import setup


setup(
name="yourpackage"
)

Yourpackage . _ _ _ init _ _. py

__version__ = '0.2.0'

Setup.cfg

[metadata]
version = attr: package.__version__

应用程序中的一些模块

from yourpackage import __version__ as expected_version
from pkg_distribution import get_distribution


installed_version = get_distribution("yourpackage").version


assert expected_version != installed_version

好像是 setuptools 不推荐再使用 pkg_resources

一个新的解决方案使用推荐的 importlib.metadata,在 Python 3.8 + 中工作:

>>> from importlib.metadata import version
>>> version('wheel')
'0.32.3'