【问题标题】:How can I get the version defined in setup.py (setuptools) in my package?如何在我的包中获取 setup.py (setuptools) 中定义的版本?
【发布时间】:2011-01-04 18:04:58
【问题描述】:

如何从我的包中获取setup.py 中定义的版本(用于--version,或其他目的)?

【问题讨论】:

标签: python setuptools


【解决方案1】:

查询已安装发行版的版本字符串

要在运行时从包中检索版本(您的问题实际上是在问什么),您可以使用:

import pkg_resources  # part of setuptools
version = pkg_resources.require("MyProject")[0].version

存储版本字符串以供安装期间使用

如果您想反其道而行之(这似乎是这里的其他答案作者似乎认为您在问的问题),请将版本字符串放在单独的文件中,并在 setup.py 中读取该文件的内容。

您可以在包中使用__version__ 行创建一个version.py,然后使用execfile('mypackage/version.py') 从setup.py 中读取它,以便在setup.py 命名空间中设置__version__

安装过程中的竞争条件警告

顺便说一句,不要按照此处另一个答案中的建议从 setup.py 导入您的包:它似乎对您有用(因为您已经安装了包的依赖项),但它会对新用户造成严重破坏您的软件包,因为如果不先手动安装依赖项,他们将无法安装您的软件包。

【讨论】:

  • execfile 工作得非常好......但(遗憾的是)不适用于 Python 3。
  • ... 好的,对于 Python 2 3,使用 with open('mypackage/version.py') as f: exec(f.read()) 代替 execfile('mypackage/version.py')。 (来自stackoverflow.com/a/437857/647002
  • 造成严重破坏的部分不一定是真的......如果您的 init.py 文件在包中具有代码导入依赖项(直接或间接),它只会造成严重破坏。
  • 似乎值得一提的是,django 执行了一个 __import__ 调用 in their setup.py。 '__import__' 与 'import' 是否可以确保安全?它似乎没有给他们带来任何问题。
  • 既然Python 3.8现在出... 标准库里直接有解决方案__version__ = importlib.metadata.version('Example')docs.python.org/3.8/library/importlib.metadata.html
【解决方案2】:

示例研究:mymodule

想象一下这个配置:

setup.py
mymodule/
        / __init__.py
        / version.py
        / myclasses.py

然后想象一些常见的场景,你有依赖,setup.py 看起来像:

setup(...
    install_requires=['dep1','dep2', ...]
    ...)

还有一个例子__init__.py

from mymodule.myclasses import *
from mymodule.version import __version__

例如myclasses.py:

# these are not installed on your system.
# importing mymodule.myclasses would give ImportError
import dep1
import dep2

问题 #1:在安装过程中导入 mymodule

如果您的setup.py 导入mymodule,那么在设置期间您很可能会得到ImportError。当您的包具有依赖项时,这是一个非常常见的错误。如果你的包没有除了内置的其他依赖,你可能是安全的;但这不是一个好习惯。原因是它不是面向未来的。说明天你的代码需要消耗一些其他的依赖。

问题 #2:我的__version__ 呢?

如果您在setup.py 中硬编码__version__,那么它可能与您在模块中发布的版本不匹配。为了保持一致,您可以将它放在一个地方,并在需要时从同一个地方读取它。使用import 可能会遇到问题#1。

解决方案:à la setuptools

您将使用openexec 的组合并为exec 提供一个字典以添加变量:

# setup.py
from setuptools import setup, find_packages
from distutils.util import convert_path

main_ns = {}
ver_path = convert_path('mymodule/version.py')
with open(ver_path) as ver_file:
    exec(ver_file.read(), main_ns)

setup(...,
    version=main_ns['__version__'],
    ...)

并在mymodule/version.py 中公开版本:

__version__ = 'some.semantic.version'

这样,版本随模块一起提供,并且在安装过程中尝试导入缺少依赖项(尚未安装)的模块时不会遇到问题。

【讨论】:

  • 这似乎是最合理的解决方案,因为它只依赖于允许的依赖关系。
  • setup.py文件中定义的包版本的概念和版本有区别吗?
【解决方案3】:

最好的方法是在您的产品代码中定义__version__,然后从那里将其导入 setup.py。这为您提供了一个可以在运行模块中读取的值,并且只有一个位置可以定义它。

setup.py 中的值没有安装,setup.py 安装后不卡住。

我在coverage.py中做了什么(例如):

# coverage/__init__.py
__version__ = "3.2"


# setup.py
from coverage import __version__

setup(
    name = 'coverage',
    version = __version__,
    ...
    )

更新(2017 年):coverage.py 不再导入自身来获取版本。导入您自己的代码可以使其可卸载,因为您的产品代码将尝试导入尚未安装的依赖项,因为 setup.py 是安装它们的。

【讨论】:

  • @Evan:我不确定您对“仅从源中提取该值”有何看法。如果带有__version__ 的文件以某种方式损坏,那么您的导入也将被损坏。 Python 不知道如何只解释您想要的语句。 @pjeby 是对的:如果您的模块需要导入其他模块,它们可能尚未安装,并且会很混乱。如果您注意导入不会导致其他导入的长链,则此技术有效。
  • @Ned Batchelder 我很确定,如果您将版本放在源文件中的任何导入之前,并且只放在“从模块导入版本”中,它将不再对源进行 lex比它需要的文件。此外,谁来发布损坏的代码?如果软件包需要依赖项,请使用 setuptools 或等待今年晚些时候 distutils2 的发布。
  • @Evan,很抱歉,您对导入部分文件有误。尝试将 print 语句放在长模块的末尾,并导入文件中定义的第一个变量。打印语句将执行。
  • 我爱上了这个,但@pjeby 是完全正确的:“顺便说一下,不要像这里另一个答案中建议的那样从你的 setup.py 导入你的包:它似乎对你有用(因为您已经安装了软件包的依赖项),但是它将对您的软件包的新用户造成严重破坏,因为如果不先手动安装依赖项,他们将无法安装您的软件包。” Ned,您介意在您的回答中添加警告吗?
  • 当我将这样的 setup.py 文件上传到 PyPI 然后 pip install <packagename> 时,就像 @Jan-PhilipGehrcke 一样,我收到以下错误:ImportError: No module named <packagename>。请警告您的读者,您不能在尚未安装软件包的环境中运行这样的 setup.py 文件!
【解决方案4】:

您的问题有点模糊,但我认为您要问的是如何指定它。

你需要像这样定义__version__

__version__ = '1.4.4'

然后您可以确认 setup.py 知道您刚刚指定的版本:

% ./setup.py --version
1.4.4

【讨论】:

    【解决方案5】:

    我对这些答案不满意...不想要求安装工具,也不想为单个变量创建一个完整的单独模块,所以我想出了这些。

    当你确定主模块是 pep8 风格并且会保持这种风格时:

    version = '0.30.unknown'
    with file('mypkg/mymod.py') as f:
        for line in f:
            if line.startswith('__version__'):
                _, _, version = line.replace("'", '').split()
                break
    

    如果您想格外小心并使用真正的解析器:

    import ast
    version = '0.30.unknown2'
    with file('mypkg/mymod.py') as f:
        for line in f:
            if line.startswith('__version__'):
                version = ast.parse(line).body[0].value.s
                break
    

    setup.py 有点像一次性模块,所以如果它有点难看也不是问题。


    更新:有趣的是,近年来我已经摆脱了这种做法,并开始使用包中名为 meta.py 的单独文件。我在那里放了很多我可能想要经常更改的元数据。所以,不仅仅是一个价值。

    【讨论】:

    • +1。它相对简单,将版本号保存在一个地方,不需要单独的文件来包含它,并且不会将 python 模块的导入依赖项强加于 setup.py。我什至不会在像 setup.py 这样短暂的程序中使用上下文管理器。
    • 比使用正则表达式更好,谢谢。您还可以使用ast.get_docstring() 和一些.split('\n')[0].strip() 等自动从源填写description。少一件事要保持同步
    【解决方案6】:

    结构如下:

    setup.py
    mymodule/
            / __init__.py
            / version.py
            / myclasses.py
    

    其中 version.py 包含:

    __version__ = 'version_string'
    

    您可以在 setup.py 中执行此操作:

    import sys
    
    sys.path[0:0] = ['mymodule']
    
    from version import __version__
    

    这不会对您在 mymodule/__init__.py 中的任何依赖项造成任何问题

    【讨论】:

      【解决方案7】:

      在源代码树中创建一个文件,例如在 yourbasedir/yourpackage/_version.py 。让该文件只包含一行代码,如下所示:

      __version__ = "1.1.0-r4704"

      然后在 setup.py 中,打开该文件并解析出版本号,如下所示:

      verstr = "未知" 尝试: verstrline = open('yourpackage/_version.py', "rt").read() 除了环境错误: pass # 好了,没有版本文件。 别的: VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" mo = re.search(VSRE, verstrline, re.M) 如果莫: verstr = mo.group(1) 别的: raise RuntimeError("unable to find version in yourpackage/_version.py")

      最后,在yourbasedir/yourpackage/__init__.py import _version 像这样:

      __version__ = “未知” 尝试: 从 _version 导入 __version__ 导入错误除外: # 我们在没有 _version.py 的树中运行,所以我们不知道我们的版本是什么。 经过

      执行此操作的代码示例是我维护的“pyutil”包。 (请参阅 PyPI 或谷歌搜索——stackoverflow 不允许我在此答案中包含指向它的超链接。)

      @pjeby 是正确的,你不应该从它自己的 setup.py 中导入你的包。当您通过创建一个新的 Python 解释器并首先在其中执行 setup.py 来测试它时,这将起作用:python setup.py,但在某些情况下它不起作用。那是因为import youpackage 并不意味着读取名为“yourpackage”的目录的当前工作目录,它意味着在当前sys.modules 中查找键“yourpackage”,然后如果不是则执行各种操作那里。因此,当您执行 python setup.py 时,它总是有效,因为您有一个新鲜的、空的 sys.modules,但这通常不起作用。

      例如,如果 py2exe 在打包应用程序的过程中执行您的 setup.py 会怎样?我见过这样的情况,其中 py2exe 会将错误的版本号放在包上,因为包在 setup.py 中从import myownthing 获取其版本号,但之前已导入该包的不同版本py2exe 运行。同样,如果 setuptools、easy_install、distribute 或 distutils2 在安装依赖于您的不同软件包的过程中尝试构建您的软件包,该怎么办?然后在评估 setup.py 时您的包是否可导入,或者在 Python 解释器的生命周期中是否已经导入了您的包的一个版本,或者导入您的包是否需要先安装其他包,或者有副作用,可以改变结果。我在尝试重用 Python 包时遇到了几次困难,这导致 py2exe 和 setuptools 等工具出现问题,因为它们的 setup.py 会导入包本身以查找其版本号。

      顺便说一句,这种技术与自动为您创建yourpackage/_version.py 文件的工具配合得很好,例如通过读取您的修订控制历史记录并根据修订控制历史记录中的最新标签写出版本号。这是一个用于 darcs 的工具:http://tahoe-lafs.org/trac/darcsver/browser/trunk/README.rst,这是一个代码 sn-p,它对 git 做同样的事情:http://github.com/warner/python-ecdsa/blob/0ed702a9d4057ecf33eea969b8cf280eaccd89a1/setup.py#L34

      【讨论】:

      • setup.py中不需要解析。只需导入该 python 文件并使用 version。导入没有副作用,因为这是该文件的唯一内容。
      【解决方案8】:

      这也应该有效,使用正则表达式并根据元数据字段具有如下格式:

      __fieldname__ = 'value'
      

      在 setup.py 的开头使用以下内容:

      import re
      main_py = open('yourmodule.py').read()
      metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", main_py))
      

      之后,您可以像这样在脚本中使用元数据:

      print 'Author is:', metadata['author']
      print 'Version is:', metadata['version']
      

      【讨论】:

      • 正如我上面所说,只需使用 str.split :)
      • 天哪,不。你是在 Python 中解析 Python 吗?至少,使用 eval(), child。
      • 糟糕的建议 slacy,应该避免 eval 这么简单。
      【解决方案9】:

      我们想将关于我们的包pypackagery 的元信息放在__init__.py 中,但不能因为它具有第三方依赖项,正如 PJ Eby 已经指出的那样(请参阅他的回答和有关竞争条件的警告)。

      我们通过创建一个单独的模块pypackagery_meta.py 解决了这个问题,该模块只包含元信息:

      """Define meta information about pypackagery package."""
      
      __title__ = 'pypackagery'
      __description__ = ('Package a subset of a monorepo and '
                         'determine the dependent packages.')
      __url__ = 'https://github.com/Parquery/pypackagery'
      __version__ = '1.0.0'
      __author__ = 'Marko Ristin'
      __author_email__ = 'marko.ristin@gmail.com'
      __license__ = 'MIT'
      __copyright__ = 'Copyright 2018 Parquery AG'
      

      然后导入packagery/__init__.py中的元信息:

      # ...
      
      from pypackagery_meta import __title__, __description__, __url__, \
          __version__, __author__, __author_email__, \
          __license__, __copyright__
      
      # ...
      

      最后用在setup.py:

      import pypackagery_meta
      
      setup(
          name=pypackagery_meta.__title__,
          version=pypackagery_meta.__version__,
          description=pypackagery_meta.__description__,
          long_description=long_description,
          url=pypackagery_meta.__url__,
          author=pypackagery_meta.__author__,
          author_email=pypackagery_meta.__author_email__,
          # ...
          py_modules=['packagery', 'pypackagery_meta'],
       )
      

      您必须使用py_modules 设置参数将pypackagery_meta 包含到您的包中。否则,您无法在安装时导入它,因为打包发行版会缺少它。

      【讨论】:

        【解决方案10】:

        为避免导入文件(从而执行其代码),可以对其进行解析并从语法树中恢复 version 属性:

        # assuming 'path' holds the path to the file
        
        import ast
        
        with open(path, 'rU') as file:
            t = compile(file.read(), path, 'exec', ast.PyCF_ONLY_AST)
            for node in (n for n in t.body if isinstance(n, ast.Assign)):
                if len(node.targets) == 1:
                    name = node.targets[0]
                    if isinstance(name, ast.Name) and \
                            name.id in ('__version__', '__version_info__', 'VERSION'):
                        v = node.value
                        if isinstance(v, ast.Str):
                            version = v.s
                            break
                        if isinstance(v, ast.Tuple):
                            r = []
                            for e in v.elts:
                                if isinstance(e, ast.Str):
                                    r.append(e.s)
                                elif isinstance(e, ast.Num):
                                    r.append(str(e.n))
                            version = '.'.join(r)
                            break
        

        此代码尝试在模块的顶层找到__version__VERSION 赋值返回字符串值。右边可以是字符串也可以是元组。

        【讨论】:

        • 这看起来既聪明又复杂 :) 只拥有一个包含一个赋值的 _version.py 文件并使用 open-read-str.split 解析它会更简单。
        • 感谢您发布此信息。我认为这对于这种情况来说太复杂了,但对于理解如何解决问题非常有帮助。我喜欢它将版本号保存在一个地方,不需要单独的文件来包含它,并且不会将 python 模块的导入依赖项强加于设置脚本。
        【解决方案11】:

        给猫剥皮有一千种方法——这是我的:

        # Copied from (and hacked):
        # https://github.com/pypa/virtualenv/blob/develop/setup.py#L42
        def get_version(filename):
            import os
            import re
        
            here = os.path.dirname(os.path.abspath(__file__))
            f = open(os.path.join(here, filename))
            version_file = f.read()
            f.close()
            version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
                                      version_file, re.M)
            if version_match:
                return version_match.group(1)
            raise RuntimeError("Unable to find version string.")
        

        【讨论】:

          【解决方案12】:

          从@gringo-suave 清理https://stackoverflow.com/a/12413800

          from itertools import ifilter
          from os import path
          from ast import parse
          
          with open(path.join('package_name', '__init__.py')) as f:
              __version__ = parse(next(ifilter(lambda line: line.startswith('__version__'),
                                               f))).body[0].value.s
          

          【讨论】:

            【解决方案13】:

            现在这很严重,需要一些改进(我什至可能错过了 pkg_resources 中未发现的成员调用),但我根本不明白为什么这不起作用,也不知道为什么到目前为止没有人建议它(谷歌搜索并没有出现这种情况)...请注意,这是 Python 2.x,需要 pkg_resources(叹气):

            import pkg_resources
            
            version_string = None
            try:
                if pkg_resources.working_set is not None:
                    disto_obj = pkg_resources.working_set.by_key.get('<my pkg name>', None)
                    # (I like adding ", None" to gets)
                    if disto_obj is not None:
                        version_string = disto_obj.version
            except Exception:
                # Do something
                pass
            

            【讨论】:

              【解决方案14】:

              简单直接,创建一个名为source/package_name/version.py的文件,内容如下:

              #!/usr/bin/env python3
              # -*- coding: UTF-8 -*-
              __version__ = "2.6.9"
              

              然后,在您的文件 source/package_name/__init__.py 上,导入版本供其他人使用:

              #!/usr/bin/env python3
              # -*- coding: UTF-8 -*-
              from .version import __version__
              

              现在,你可以把它放在setup.py

              #!/usr/bin/env python3
              # -*- coding: UTF-8 -*-
              import re
              import sys
              
              try:
                  filepath = 'source/package_name/version.py'
                  version_file = open( filepath )
                  __version__ ,= re.findall( '__version__ = "(.*)"', version_file.read() )
              
              except Exception as error:
                  __version__ = "0.0.1"
                  sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )
              
              finally:
                  version_file.close()
              

              在 Linux、Windows 和 Mac OS 上使用 Python 2.73.33.43.53.63.7 对此进行了测试。我在我的包上使用了所有这些平台的集成和单元测试。你可以在这里看到.travis.ymlappveyor.yml的结果:

              1. https://travis-ci.org/evandrocoan/debugtools/builds/527110800
              2. https://ci.appveyor.com/project/evandrocoan/pythondebugtools/builds/24245446

              另一个版本正在使用上下文管理器:

              #!/usr/bin/env python3
              # -*- coding: UTF-8 -*-
              import re
              import sys
              
              try:
                  filepath = 'source/package_name/version.py'
              
                  with open( filepath ) as file:
                      __version__ ,= re.findall( '__version__ = "(.*)"', file.read() )
              
              except Exception as error:
                  __version__ = "0.0.1"
                  sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )
              

              您还可以使用 codecs 模块来处理 Python 2.73.6 上的 unicode 错误

              #!/usr/bin/env python3
              # -*- coding: UTF-8 -*-
              import re
              import sys
              import codecs
              
              try:
                  filepath = 'source/package_name/version.py'
              
                  with codecs.open( filepath, 'r', errors='ignore' ) as file:
                      __version__ ,= re.findall( '__version__ = "(.*)"', file.read() )
              
              except Exception as error:
                  __version__ = "0.0.1"
                  sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )
              

              如果您使用 Python C 扩展 100% 使用 C/C++ 编写 Python 模块,您可以做同样的事情,但使用 C/C++ 而不是 Python。

              在这种情况下,创建以下setup.py

              #!/usr/bin/env python3
              # -*- coding: UTF-8 -*-
              import re
              import sys
              import codecs
              from setuptools import setup, Extension
              
              try:
                  filepath = 'source/version.h'
              
                  with codecs.open( filepath, 'r', errors='ignore' ) as file:
                      __version__ ,= re.findall( '__version__ = "(.*)"', file.read() )
              
              except Exception as error:
                  __version__ = "0.0.1"
                  sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )
              
              setup(
                      name = 'package_name',
                      version = __version__,
              
                      package_data = {
                              '': [ '**.txt', '**.md', '**.py', '**.h', '**.hpp', '**.c', '**.cpp' ],
                          },
              
                      ext_modules = [
                          Extension(
                              name = 'package_name',
                              sources = [
                                  'source/file.cpp',
                              ],
                              include_dirs = ['source'],
                          )
                      ],
                  )
              

              从文件version.h读取版本:

              const char* __version__ = "1.0.12";
              

              但是,不要忘记创建 MANIFEST.in 以包含 version.h 文件:

              include README.md
              include LICENSE.txt
              
              recursive-include source *.h
              

              它被集成到主应用程序中:

              #include <Python.h>
              #include "version.h"
              
              // create the module
              PyMODINIT_FUNC PyInit_package_name(void)
              {
                  PyObject* thismodule;
                  ...
              
                  // https://docs.python.org/3/c-api/arg.html#c.Py_BuildValue
                  PyObject_SetAttrString( thismodule, "__version__", Py_BuildValue( "s", __version__ ) );
              
                  ...
              }
              

              参考资料:

              1. python open file error
              2. Define a global in a Python module from a C API
              3. How to include package data with setuptools/distribute?
              4. https://github.com/lark-parser/lark/blob/master/setup.py#L4
              5. How to use setuptools packages and ext_modules with the same name?
              6. Is it possible to include subdirectories using dist utils (setup.py) as part of package data?

              【讨论】:

                【解决方案15】:

                将包部署到服务器和索引包的文件命名约定:

                pip动态版本转换示例:

                • 赢:

                  • test_pkg-1.0.0-cp36-cp36m-win_amd64.whl
                  • test_pkg-1.0.0-py3.6-win-amd64.egg
                • mac:

                  • test_pkg-1.0.0-py3.7-macosx-10.12-x86_64.egg
                  • test_pkg-1.0.0-py3.7-macosx-10.12-x86_64.whl
                • Linux:
                  • test_pkg-1.0.0-cp36-cp36m-linux_x86_64.whl
                from setuptools_scm import get_version
                
                def _get_version():
                
                     dev_version = str(".".join(map(str, str(get_version()).split("+")[0]\
                            .split('.')[:-1])))
                
                    return dev_version
                
                

                找到示例 setup.py 从 git commit 调用动态 pip 版本匹配

                setup(
                    version=_get_version(),
                    name=NAME,
                    description=DESCRIPTION,
                    long_description=LONG_DESCRIPTION,
                    classifiers=CLASSIFIERS,
                
                # add few more for wheel wheel package ...conversion
                
                )
                

                【讨论】:

                  【解决方案16】:

                  我正在使用如下环境变量

                  VERSION=0.0.0 python setup.py sdist bdist_wheel

                  在 setup.py 中

                  
                  import os
                  
                  setup(
                      version=os.environ['VERSION'],
                      ...
                  )
                  
                  

                  为了与打包程序版本进行一致性检查,我使用以下脚本。

                  PKG_VERSION=`python -c "import pkg; print(pkg.__version__)"`
                  if [ $PKG_VERSION == $VERSION ]; then
                      python setup.py sdist bdist_wheel
                  else
                      echo "Package version differs from set env variable"
                  fi
                  

                  【讨论】:

                    猜你喜欢
                    • 2011-10-10
                    • 1970-01-01
                    • 2011-08-23
                    • 1970-01-01
                    • 2011-09-02
                    • 1970-01-01
                    • 2020-12-17
                    • 2012-06-29
                    • 2018-11-24
                    相关资源
                    最近更新 更多