与setup.py和包共享包版本的正确方法是什么?
对于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中,但是我还想在我的包中添加额外的导入来创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
,并使用ast
parsingsetup.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
上的封装指南中提出了几种方法。