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

时间:2021-08-15 07:30:33

With distutils, setuptools, etc. a package version is specified in setup.py:

对于distutils、setuptools等,在setup.py中指定了一个包版本:

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

I would like to be able to access the same version number from within the package:

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

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

I could add __version__ = '1.0.0' to my package's __init__.py, but I would also like to include additional imports in my package to create a simplified interface to the package:

我可以在包的__init__中添加__version__ = '1.0.0'。py,但是我还想在我的包中包含额外的导入,以便为包创建一个简化的接口:

# file: __init__.py

from foobar import foo
from foobar.bar import Bar

__version__ = '1.0.0'

and

# file: setup.py

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

However, these additional imports can cause the installation of foobar to fail if they import other packages that are not yet installed. What is the correct way to share package version with setup.py and the package?

但是,这些附加的导入可能导致foobar安装失败,如果它们导入了尚未安装的其他包。与setup共享包版本的正确方法是什么?py和包吗?

7 个解决方案

#1


62  

Set the version in setup.py only, and read your own version with pkg_resources, effectively querying the setuptools metadata:

在安装中设置版本。只需py,并使用pkg_resources读取您自己的版本,有效地查询setuptools元数据:

file: setup.py

文件:setup . py

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

file: __init__.py

文件:__init__ . py

from pkg_resources import get_distribution

__version__ = get_distribution('foobar').version

To make this work in all cases, where you could end up running this without having installed it, test for DistributionNotFound and the distribution location:

为了在所有情况下完成这项工作,您可以在没有安装它的情况下运行它,测试分配notfound和分布位置:

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

#2


16  

I don't believe there's a canonical answer to this, but my method (either directly copied or slightly tweaked from what I've seen in various other places) is as follows:

我不相信这是一个典型的答案,但是我的方法(不是直接复制,就是从我在其他地方看到的东西稍微调整)如下:

Folder heirarchy (relevant files only):

文件夹继承人制度(仅适用于相关文件):

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

main_package/_version.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:

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:

setup . py:

from setuptools import setup

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

... which is ugly as sin ... but it works, and I've seen it or something like it in packages distributed by people who I'd expect to know a better way if there were one.

…就像罪恶一样丑陋……但它确实有效,我在包里见过它或者类似的东西,这些包是由我希望知道更好的方法的人分发的,如果有的话。

#3


8  

I agree with @stefano-m 's philosophy about:

我同意@stefano-m关于:

Having version = "x.y.z" in the source and parsing it within setup.py is definitely the correct solution, IMHO. Much better than (the other way around) relying on run time magic.

在version = " x.y。z"在源文件中,在设置中解析。py绝对是正确的解决方案,IMHO。比依赖运行时魔法要好得多。

And this answer is derived from @zero-piraeus 's answer. The whole point is "don't use imports in setup.py, instead, read the version from a file".

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

I use regex to parse the __version__ so that it does not need to be the last line of a dedicated file at all. In fact, I still put the single-source-of-truth __version__ inside my project's __init__.py.

我使用regex解析__version__,这样它就不需要成为专用文件的最后一行。事实上,我仍然在我的项目的__init__.py中加入了单源真实版本。

Folder heirarchy (relevant files only):

文件夹继承人制度(仅适用于相关文件):

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

main_package/__init__.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:

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

... which is still not ideal ... but it works.

…这仍然不理想……但它的工作原理。

And by the way, at this point you can test your new toy in this way:

顺便说一下,现在你可以这样测试你的新玩具:

python setup.py --version
1.2.3

PS: This official Python packaging document (and its mirror) describes more options. Its first option is also using regex. (Depends on the exact regex you use, it may or may not handle quotation marks inside version string. Generally not a big issue though.)

这个官方的Python打包文档(及其镜像)描述了更多的选项。它的第一个选项也是使用regex。(取决于您使用的确切regex,它可能处理版本字符串中的引号,也可能不处理。一般来说,这并不是什么大问题。

PPS: The fix in ADAL Python is now backported into this answer.

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

#4


3  

Put __version__ in your_pkg/__init__.py, and parse in setup.py using ast:

把__version__ your_pkg / __init__。py,并在设置中解析。使用ast 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://*.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],
      ...
      )

If using Python < 3.4, note that importlib.util.find_spec is not available. Moreover, any backport of importlib of course cannot be relied upon to be available to setup.py. In this case, use:

如果使用Python < 3.4,请注意importli .util。find_spec是不可用的。而且,importlib的任何支持都不能用于setup.py。在这种情况下,使用:

import os

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

#5


2  

Based on the accepted answer and comments, this is what I ended up doing:

基于大家接受的回答和评论,我最后做的是:

file: setup.py

文件:setup . py

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

file: __init__.py

文件:__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

Explanation:

解释:

  • __project__ is the name of the project to install which may be different than the name of the package

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

  • VERSION is what I display in my command-line interfaces when --version is requested

    版本是我在命令行界面中显示的——请求版本。

  • the additional imports (for the simplified package interface) only occur if the project has actually been installed

    额外的导入(对于简化的包接口)只有在项目实际安装时才会发生。

#6


2  

There are several methods proposed in the Packaging guides on python.org.

在python.org上的包装指南中提出了几种方法。

#7


0  

The accepted answer requires that the package has been installed. In my case, I needed to extract the installation params (including __version__) from the source setup.py. I found a direct and simple solution while looking through the tests of the setuptools package. Looking for more info on the _setup_stop_after attribute lead me to an old mailing list post which mentioned distutils.core.run_setup, which lead me to the actual docs needed. After all that, here's the simple solution:

被接受的答案要求已安装包。在我的例子中,我需要从源setup.py中提取安装参数(包括__version__)。在查看setuptools包的测试时,我找到了一个直接而简单的解决方案。查找关于_setup_stop_after属性的更多信息,我找到了一个旧的邮件列表,其中提到了distutil .core.run_setup,它将我引向所需的实际文档。在这之后,有一个简单的解决办法:

file setup.py:

文件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)

file extract.py:

文件extract.py:

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

#1


62  

Set the version in setup.py only, and read your own version with pkg_resources, effectively querying the setuptools metadata:

在安装中设置版本。只需py,并使用pkg_resources读取您自己的版本,有效地查询setuptools元数据:

file: setup.py

文件:setup . py

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

file: __init__.py

文件:__init__ . py

from pkg_resources import get_distribution

__version__ = get_distribution('foobar').version

To make this work in all cases, where you could end up running this without having installed it, test for DistributionNotFound and the distribution location:

为了在所有情况下完成这项工作,您可以在没有安装它的情况下运行它,测试分配notfound和分布位置:

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

#2


16  

I don't believe there's a canonical answer to this, but my method (either directly copied or slightly tweaked from what I've seen in various other places) is as follows:

我不相信这是一个典型的答案,但是我的方法(不是直接复制,就是从我在其他地方看到的东西稍微调整)如下:

Folder heirarchy (relevant files only):

文件夹继承人制度(仅适用于相关文件):

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

main_package/_version.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:

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:

setup . py:

from setuptools import setup

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

... which is ugly as sin ... but it works, and I've seen it or something like it in packages distributed by people who I'd expect to know a better way if there were one.

…就像罪恶一样丑陋……但它确实有效,我在包里见过它或者类似的东西,这些包是由我希望知道更好的方法的人分发的,如果有的话。

#3


8  

I agree with @stefano-m 's philosophy about:

我同意@stefano-m关于:

Having version = "x.y.z" in the source and parsing it within setup.py is definitely the correct solution, IMHO. Much better than (the other way around) relying on run time magic.

在version = " x.y。z"在源文件中,在设置中解析。py绝对是正确的解决方案,IMHO。比依赖运行时魔法要好得多。

And this answer is derived from @zero-piraeus 's answer. The whole point is "don't use imports in setup.py, instead, read the version from a file".

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

I use regex to parse the __version__ so that it does not need to be the last line of a dedicated file at all. In fact, I still put the single-source-of-truth __version__ inside my project's __init__.py.

我使用regex解析__version__,这样它就不需要成为专用文件的最后一行。事实上,我仍然在我的项目的__init__.py中加入了单源真实版本。

Folder heirarchy (relevant files only):

文件夹继承人制度(仅适用于相关文件):

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

main_package/__init__.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:

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

... which is still not ideal ... but it works.

…这仍然不理想……但它的工作原理。

And by the way, at this point you can test your new toy in this way:

顺便说一下,现在你可以这样测试你的新玩具:

python setup.py --version
1.2.3

PS: This official Python packaging document (and its mirror) describes more options. Its first option is also using regex. (Depends on the exact regex you use, it may or may not handle quotation marks inside version string. Generally not a big issue though.)

这个官方的Python打包文档(及其镜像)描述了更多的选项。它的第一个选项也是使用regex。(取决于您使用的确切regex,它可能处理版本字符串中的引号,也可能不处理。一般来说,这并不是什么大问题。

PPS: The fix in ADAL Python is now backported into this answer.

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

#4


3  

Put __version__ in your_pkg/__init__.py, and parse in setup.py using ast:

把__version__ your_pkg / __init__。py,并在设置中解析。使用ast 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://*.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],
      ...
      )

If using Python < 3.4, note that importlib.util.find_spec is not available. Moreover, any backport of importlib of course cannot be relied upon to be available to setup.py. In this case, use:

如果使用Python < 3.4,请注意importli .util。find_spec是不可用的。而且,importlib的任何支持都不能用于setup.py。在这种情况下,使用:

import os

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

#5


2  

Based on the accepted answer and comments, this is what I ended up doing:

基于大家接受的回答和评论,我最后做的是:

file: setup.py

文件:setup . py

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

file: __init__.py

文件:__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

Explanation:

解释:

  • __project__ is the name of the project to install which may be different than the name of the package

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

  • VERSION is what I display in my command-line interfaces when --version is requested

    版本是我在命令行界面中显示的——请求版本。

  • the additional imports (for the simplified package interface) only occur if the project has actually been installed

    额外的导入(对于简化的包接口)只有在项目实际安装时才会发生。

#6


2  

There are several methods proposed in the Packaging guides on python.org.

在python.org上的包装指南中提出了几种方法。

#7


0  

The accepted answer requires that the package has been installed. In my case, I needed to extract the installation params (including __version__) from the source setup.py. I found a direct and simple solution while looking through the tests of the setuptools package. Looking for more info on the _setup_stop_after attribute lead me to an old mailing list post which mentioned distutils.core.run_setup, which lead me to the actual docs needed. After all that, here's the simple solution:

被接受的答案要求已安装包。在我的例子中,我需要从源setup.py中提取安装参数(包括__version__)。在查看setuptools包的测试时,我找到了一个直接而简单的解决方案。查找关于_setup_stop_after属性的更多信息,我找到了一个旧的邮件列表,其中提到了distutil .core.run_setup,它将我引向所需的实际文档。在这之后,有一个简单的解决办法:

file setup.py:

文件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)

file extract.py:

文件extract.py:

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