使用setup.py和软件包共享软件包版本的正确方法是什么?

Jac*_*ing 62 python distutils setuptools

使用distutils,setuptools等,包版本指定在setup.py:

# file: setup.py
...
setup(
name='foobar',
version='1.0.0',
# other attributes
)
Run Code Online (Sandbox Code Playgroud)

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

>>> import foobar
>>> foobar.__version__
'1.0.0'
Run Code Online (Sandbox Code Playgroud)

我可以添加__version__ = '1.0.0'到我的包的__init__.py,但我还想在我的包中包含其他导入以创建包的简化接口:

# file: __init__.py

from foobar import foo
from foobar.bar import Bar

__version__ = '1.0.0'
Run Code Online (Sandbox Code Playgroud)

# file: setup.py

from foobar import __version__
...
setup(
name='foobar',
version=__version__,
# other attributes
)
Run Code Online (Sandbox Code Playgroud)

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

Mar*_*ers 74

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

文件: setup.py

setup(
    name='foobar',
    version='1.0.0',
    # other attributes
)
Run Code Online (Sandbox Code Playgroud)

文件: __init__.py

from pkg_resources import get_distribution

__version__ = get_distribution('foobar').version
Run Code Online (Sandbox Code Playgroud)

要在所有情况下都能正常工作,您最终可以在不安装它的情况下运行它,测试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
Run Code Online (Sandbox Code Playgroud)

  • 我不喜欢这个解决方案:`__version__`在运行时而不是在构建时解决.恕我直言我更喜欢在源代码树中有一个静态的`__version__`,并在构建时用`setup.py`中的一些代码读取它,如[answer]中所示(http://stackoverflow.com/a/17626524/1499402 )下面. (14认同)
  • 我不得不同意@StefanoM这个解决方案是次优的.我觉得它具有误导性的原因是,如果你的系统上同时安装了一个版本和一个开发版本,它将始终显示已安装的版本,无论实际导入哪一个. (8认同)
  • 我的意思是`__version__ ="xyz"`(在构建时由`setup.py`解析一次)和`__version__ = some_weird_function()`之间的区别,它在运行时被评估以恢复仅在` setup.py`和`foobar.egg-info`. (3认同)
  • 同意:我的措辞不正确,因为Python是一种解释性语言.然而,重要的是要注意构建时可能的失败(如果`setup.py`无法解析`__version__ ='xyz'`)或运行时(如果`get_distribution('foobar'))之间的差异.无法恢复正确的信息.)您的方法当然有很多优点,例如在构建时更改版本号的可能性:`python setup.py build --tag-date`.什么必须进入`__version__`:在源代码树中烧毁的东西,或者在构建时计算并在运行时恢复的一些元数据? (3认同)
  • 如果这实际上可靠地工作,它比我的答案更优雅...这让我想知道为什么我没有在其他地方看到它.有谁知道[这个](https://github.com/pypa/pip/issues/401)是否真的值得关注?如果它确实报告错误的版本号,它的优雅既不在这里也不在那里...... (2认同)
  • @greschd:不过,您的开发版本应该与 `setup.py develop` 一起安装。 (2认同)
  • @greschd 始终使用 virtualenv!您不再需要全局安装软件包了。 (2认同)
  • 这种方法可能对使用`python setup.py install`或`pip install`很敏感.在前一种情况下,已经观察到失败.可能与[`pip` issue 3045](https://github.com/pypa/pip/issues/3045)有关. (2认同)
  • 很好,但遗憾的是它通过违反当前的[PEP8建议](https://www.python.org/dev/peps/pep-0008/#module-level-dunder-names)唤醒了大地精:`__ version__等.应该放在模块文档字符串之后但是在除__future__进口之外的任何import语句之前.(这里我们必须在定义`__version__`之前导入pkg_resources) (2认同)

Zer*_*eus 18

我不相信这有一个规范的答案,但我的方法(直接复制或稍微调整我在其他地方看到的)如下:

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

package_root/
 |- main_package/
 |   |- __init__.py
 |   `- _version.py
 `- setup.py
Run Code Online (Sandbox Code Playgroud)

main_package/_version.py:

"""Version information."""

# The following line *must* be the last in the module, exactly as formatted:
__version__ = "1.0.0"
Run Code Online (Sandbox Code Playgroud)

main_package/__init__.py:

"""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.
)
Run Code Online (Sandbox Code Playgroud)

setup.py:

from setuptools import setup

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

......作为罪恶是丑陋的......但是它有效,而且我已经在人们分发的包裹中看到它或类似的东西,如果有的话,我希望知道更好的方式.

  • 在源代码中使用`__version__ ="xyz"`并在`setup.py`中解析它绝对是*正确的解决方案,恕我直言.依靠运行时魔法更好. (9认同)
  • **tl; dr:不要在setup.py中使用导入,从文件中读取版本.**我需要考虑一段时间来决定我是否喜欢这种方法... (4认同)
  • @JaceBrowning 是的,这是一个公平的总结......我怀疑任何解决方案都必须是这个的变体,因为它在 setup.py 中导入导致问题的包。 (2认同)

Ray*_*Luo 13

我同意@ stefano-m的哲学:

在源代码中拥有version ="xyz"并在setup.py中解析它绝对是正确的解决方案,恕我直言.比依赖运行时魔术(反过来)要好得多.

这个答案来自@ zero-piraeus的回答.重点是"不要在setup.py中使用导入,而是从文件中读取版本".

我使用正则表达式来解析__version__它,所以它根本不需要是专用文件的最后一行.事实上,我仍然把单一的事实来源__version__放在我的项目中__init__.py.

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

package_root/
 |- main_package/
 |   `- __init__.py
 `- setup.py
Run Code Online (Sandbox Code Playgroud)

main_package/__init__.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.
)
Run Code Online (Sandbox Code Playgroud)

setup.py:

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.
)
Run Code Online (Sandbox Code Playgroud)

......这仍然不理想......但它确实有效.

顺便说一句,此时你可以用这种方式测试你的新玩具:

python setup.py --version
1.2.3
Run Code Online (Sandbox Code Playgroud)

PS:这个官方Python打包文档(及其镜像)描述了更多选项.它的第一个选择也是使用正则表达式.(取决于你使用的确切正则表达式,它可能会也可能不会处理版本字符串中的引号.虽然通常不是一个大问题.)

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

  • 这对我来说似乎也是最优雅的方法.谢谢 (3认同)
  • 这个链接似乎是(可能的)官方Python指南的一些镜像:https://packaging.python.org/single_source_version/ (2认同)

inf*_*ith 9

setuptools 46.4.0 添加了基本的抽象语法树分析支持,以便setup.cfg attr: 指令无需导入包的依赖项即可工作。这使得可以拥有软件包版本的单一真实来源,从而使 setupstools 46.4.0 发布之前发布的先前答案中的许多解决方案过时。

如果 __version__ 在 yourpackage.__init__.py 中初始化,并且以下元数据添加到包的 setup.cfg 文件中,现在可以避免将版本传递给 setup.py 中的 setuptools.setup 函数。通过此配置,setuptools.setup 函数将自动从 yourpackage.__init__.py 解析包版本,并且您可以在应用程序中需要的地方自由导入 __version__.py。

例子

setup.py没有传递给 setup 的版本

from setuptools import setup

setup(
    name="yourpackage"
)
Run Code Online (Sandbox Code Playgroud)

你的包.____init__.py

__version__ = '0.2.0'
Run Code Online (Sandbox Code Playgroud)

安装程序.cfg

[metadata]
version = attr: package.__version__
Run Code Online (Sandbox Code Playgroud)

您的应用程序中的某些模块

from yourpackage import __version__ as expected_version
from pkg_distribution import get_distribution

installed_version = get_distribution("yourpackage").version

assert expected_version != installed_version
Run Code Online (Sandbox Code Playgroud)