74

使用distutils,setuptools等指定包版本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 和包共享包版本的正确方法是什么?

4

9 回答 9

89

仅设置版本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
于 2013-07-14T09:44:25.397 回答
23

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

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

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

main_package/_version.py

"""Version information."""

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

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

setup.py

from setuptools import setup

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

......这就像罪恶一样丑陋......但它有效,而且我已经在由人们分发的包裹中看到它或类似的东西,如果有的话,我希望他们知道更好的方法。

于 2013-07-13T02:54:54.483 回答
18

我同意@stefano-m关于以下方面的哲学:

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

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

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

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

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

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

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

......这仍然不理想......但它有效。

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

python setup.py --version
1.2.3

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

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

于 2016-09-24T00:12:41.143 回答
4

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

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

例子

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

from setuptools import setup

setup(
    name="yourpackage"
)

你的包.____ init__.py

__version__ = 0.2.0

设置.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
于 2021-01-08T07:59:52.587 回答
3

接受的答案要求已安装软件包。就我而言,我需要__version__从源代码中提取安装参数(包括 )setup.py在查看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()
于 2018-04-27T16:06:02.163 回答
3

放入,并在使用__version__your_pkg/__init__.py解析:setup.pyast

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')
于 2017-02-16T09:00:34.833 回答
1

根据接受的答案和评论,这就是我最终要做的事情:

文件: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被请求时在命令行界面中显示的内容

  • 额外的导入(对于简化的包界面)仅在项目实际安装时发生

于 2013-07-14T14:29:52.427 回答
1

好像setuptools 不推荐再使用pkg_resources

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

>>> from importlib.metadata import version  
>>> version('wheel')  
'0.32.3'
于 2021-09-30T13:02:07.233 回答
-1

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

模块/版本.py:

__version__ = "1.0.2"

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

模块/__init__.py:

from . import version
__version__ = version.__version__

设置.py:

import subprocess

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

对我来说主要优势是它不需要手工解析或正则表达式或 manifest.in 条目。它也是相当 Pythonic,似乎在所有情况下都有效(pip -e 等),并且可以通过在 version.py 中使用 argparse 轻松扩展以共享文档字符串等。任何人都可以看到这种方法的问题吗?

于 2020-04-22T16:52:35.343 回答