与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中,但是我还想在我的包中添加额外的导入来创build一个简化的包接口:

 # 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和包共享包版本的正确方法是什么?

仅在setup.py设置版本,并使用pkg_resources读取您自己的版本, 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 

为了在所有情况下都能正常工作,在没有安装的情况下最终可以运行此操作,请testingDistributionNotFound和分发位置:

 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 

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

文件夹heirarchy(仅适用于相关文件):

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

…这是丑陋的罪恶…但它的工作原理,我已经看到它或类似的东西分发给人,如果有一个更好的方式,我希望知道。

我同意@ stefano-m的哲学:

在源文件中有版本 =“xyz”并在setup.py中parsing它绝对是正确的解决scheme,恕我直言。 依靠运行时间魔术比(相反)好得多。

这个答案来自@零比雷埃夫斯的答案 。 整个问题是“不要在setup.py中使用import,而应该从文件中读取版本”。

我使用正则expression式来parsing__version__所以它不需要成为专用文件的最后一行。 事实上,我仍然把我的项目的__init__.py的单一来源的__version__

文件夹heirarchy(仅适用于相关文件):

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

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

顺便说一句,在这一点上,你可以用这种方式testing你的新玩具:

 python setup.py --version 1.2.3 

PS:这个Python包装(官方?)文档描述了更多的选项。 它的第一个选项是使用正则expression式。 (取决于你使用的确切的正则expression式,它可能或不可能处理版本string中的引号,但通常不是一个大问题。

PPS: 在ADAL Python中的修复现在被回传到这个答案中。

基于接受的答案和评论,这是我最终做的:

文件: 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被请求的时候

  • 只有在实际安装了项目的情况下才会出现额外的导入(对于简化的包界面)

__version__放入your_pkg/__init__.py ,并使用astparsingsetup.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的backport当然都不能依赖于setup.py 。 在这种情况下,使用:

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

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