簡體   English   中英

如何在我的 package 中獲取 setup.py (setuptools) 中定義的版本?

[英]How can I get the version defined in setup.py (setuptools) in my package?

我如何從我的 package 中獲取setup.py中定義的版本(用於--version或其他目的)?

查詢已安裝發行版的版本字符串

要在運行時從包內部檢索版本(您的問題實際上是在問什么),您可以使用:

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 導入您的包:它似乎對您有用(因為您已經安裝了包的依賴項),但它會對您的包的新用戶造成嚴重破壞,因為如果不先手動安裝依賴項,他們將無法安裝您的包。

示例研究: 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提供一個 dict 來添加變量:

# 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'

這樣,該版本隨模塊一起提供,並且您在安裝過程中嘗試導入缺少依賴項(尚未安裝)的模塊時不會遇到問題。

最好的技術是在您的產品代碼中定義__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 是安裝它們的。

你的問題有點模糊,但我認為你要問的是如何指定它。

您需要像這樣定義__version__

__version__ = '1.4.4'

然后您可以確認 setup.py 知道您剛剛指定的版本:

% ./setup.py --version
1.4.4

我對這些答案不滿意......不想要求 setuptools,也不想為單個變量制作一個完整的單獨模塊,所以我想出了這些。

因為當你確定主模塊是 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的單獨文件。 我在那里放了很多我可能想要經常更改的元數據。 所以,不僅僅是一個價值。

使用這樣的結構:

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 中的任何依賴項造成任何問題

在您的源代碼樹中創建一個文件,例如在 yourbasedir/yourpackage/_version.py 中。 讓該文件只包含一行代碼,如下所示:

__version__ = "1.1.0-r4704"

然后在 setup.py 中,打開該文件並解析出版本號,如下所示:

verstr = "unknown"
try:
    verstrline = open('yourpackage/_version.py', "rt").read()
except EnvironmentError:
    pass # Okay, there is no version file.
else:
    VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
    mo = re.search(VSRE, verstrline, re.M)
    if mo:
        verstr = mo.group(1)
    else:
        raise RuntimeError("unable to find version in yourpackage/_version.py")

最后,在yourbasedir/yourpackage/__init__.py導入 _version,如下所示:

__version__ = "unknown"
try:
    from _version import __version__
except ImportError:
    # We're running in a tree that doesn't have a _version.py, so we don't know what our version is.
    pass

執行此操作的代碼示例是我維護的“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 ,這是對 git 執行相同操作的代碼片段: http://github .com/warner/python-ecdsa/blob/0ed702a9d4057ecf33eea969b8cf280eaccd89a1/setup.py#L34

我們想將關於我們的包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包含到您的包中。 否則,您無法在安裝時導入它,因為打包發行版會缺少它。

這也應該起作用,使用正則表達式並根據元數據字段具有如下格式:

__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']

簡單直接,創建一個名為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 3.7 3.4 2.7 3.3 3.5 3.6 我在我的包上使用了所有這些平台的集成和單元測試。 您可以在此處查看.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打開文件錯誤
  2. 從 C API 在 Python 模塊中定義全局變量
  3. 如何在 setuptools/distribute 中包含包數據?
  4. https://github.com/lark-parser/lark/blob/master/setup.py#L4
  5. 如何使用同名的 setuptools 包和 ext_modules?
  6. 是否可以使用 dist utils (setup.py) 包含子目錄作為包數據的一部分?

給貓剝皮有一千種方法——這是我的:

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

為了避免導入文件(並因此執行其代碼),可以對其進行解析並從語法樹中恢復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賦值是字符串值。 右側可以是字符串或元組。

從@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

現在這很嚴重,需要一些改進(我什至錯過了 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

將包部署到服務器和索引包的文件命名約定:

pip 動態版本轉換示例:

  • 贏:

    • test_pkg-1.0.0-cp36-cp36m-win_amd64.whl
    • test_pkg-1.0.0-py3.6-win-amd64.egg
  • 蘋果電腦:

    • 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

)

我正在使用如下環境變量

版本=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

值得一提的是,我寫了getversion來解決這個問題,以滿足我們項目的一個需求。 它依賴於一系列符合 PEP 的策略來返回模塊的版本,並為開發模式 (git scm) 添加了一些策略。

例子:

from getversion import get_module_version

# Get the version of an imported module
from xml import dom
version, details = get_module_version(dom)
print(version)

產量

3.7.3.final.0

為什么找到這個版本? 你可以從details上理解:

> print(details)
Version '3.7.3.final.0' found for module 'xml.dom' by strategy 'get_builtin_module_version', after the following failed attempts:
 - Attempts for module 'xml.dom':
   - <get_module_version_attr>: module 'xml.dom' has no attribute '__version__'
 - Attempts for module 'xml':
   - <get_module_version_attr>: module 'xml' has no attribute '__version__'
   - <get_version_using_pkgresources>: Invalid version number: None
   - <get_builtin_module_version>: SUCCESS: 3.7.3.final.0

更多可以在文檔中找到。

您應該在您的包中包含版本字符串,例如__version__ = 'major.minor.patch' in __init__.py ,而不是在您的代碼中檢索 setup 中定義的版本。

然后你可以在 setup.py 中導入你的包並傳遞version = your_package.__version__ ,或者你可以在 setup.cfg 中使用version = attr: your_package.__version__

我創建了正則表達式模式以從 setup.cfg 中查找版本號?:[\s]+|[\s])?[=](?:[\s]+|[\s])?(.*)

import re
with open("setup.cfg", "r") as _file:
    data = _file.read()
print(re.findall(r"\nversion(?:[\s]+|[\s])?[=](?:[\s]+|[\s])?(.*)", data))
# -> ['1.1.0']

許多其他答案都已過時,我相信從已安裝的 python 3.10 package 獲取版本信息的標准方法是使用PEP-0566起的 importlib.metadata

官方文檔: https://docs.python.org/3.10/library/importlib.metadata.html

from importlib.metadata import version
VERSION_NUM = version("InstalledPackageName")

這很簡單,干凈,沒有大驚小怪。

如果您在 package 安裝期間運行的腳本中做一些奇怪的事情,這將不起作用,但是如果您所做的只是獲取版本號以通過 CLI --help 命令向用戶顯示版本檢查,關於框, 或者其他已經安裝了 package 並且只需要安裝的版本號的東西,這對我來說似乎是最好的解決方案。

您可以將此代碼添加到您的__init__.py

VERSION = (0, 3, 0)


def get_version():
    """Return the VERSION as a string.
    For example, if `VERSION == (0, 10, 7)`, return '0.10.7'.
    """
    return ".".join(map(str, VERSION))


__version__ = get_version()

並將其添加到setup.py中:

def get_version(version_tuple):
    """Return the version tuple as a string, e.g. for (0, 10, 7),
    return '0.10.7'.
    """
    return ".".join(map(str, version_tuple))


init = os.path.join(os.path.dirname(__file__), "your_library", "__init__.py")
version_line = list(filter(lambda line: line.startswith("VERSION"), open(init)))[0]
VERSION = get_version(eval(version_line.split("=")[-1]))

最后,您可以將version=VERSION,行添加到setup中:

setup(
    name="your_library",
    version=VERSION,
)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM