Каков правильный способ обмена версией пакета с помощью 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, но также хотел бы включить в мой пакет дополнительные импортные файлы для создания упрощенного интерфейса к пакету:

# 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 и пакета?

Ответ 1

Задайте версию только в setup.py и прочитайте свою собственную версию с pkg_resources, эффективно запросив метаданные setuptools:

file: setup.py

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

файл: __init__.py

from pkg_resources import get_distribution

__version__ = get_distribution('foobar').version

Чтобы сделать эту работу во всех случаях, когда вы можете запустить ее без ее установки, проверьте DistributionNotFound и расположение распространения:

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

Я не верю, что есть канонический ответ на этот вопрос, но мой метод (прямо или скопированный или слегка измененный из того, что я видел в других местах) выглядит следующим образом:

Папка иерархии (только для соответствующих файлов):

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

..., который является уродливым как грех... но он работает, и я видел его или что-то подобное в пакетах, распространяемых людьми, которых я ожидал бы узнать лучше, если бы они были.

Ответ 3

Я согласен с философией @stefano-m о:

Имея версию= "x.y.z" в источнике и анализируя его внутри setup.py, безусловно, является правильным решением, IMHO. Гораздо лучше, чем (наоборот), полагаясь на магию времени выполнения.

И этот ответ получен из @zero-piraeus answer. Все дело в том, что "не используйте импорт в setup.py, а не читайте версию из файла".

Я использую regex для синтаксического анализа __version__, так что он не обязательно должен быть последней строкой выделенного файла. На самом деле, я все еще помещаю один источник истины __version__ в свой проект __init__.py.

Папка иерархии (только для соответствующих файлов):

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

... который по-прежнему не идеален... но он работает.

И, кстати, на этом этапе вы можете протестировать свою новую игрушку таким образом:

python setup.py --version
1.2.3

PS: Этот Документ Python (официальный?) описывает больше параметров. Его первый вариант также использует регулярное выражение. (Зависит от точного регулярного выражения, которое вы используете, оно может или не может обрабатывать кавычки внутри строки версии. Как правило, это не большая проблема.)

PPS: fix в ADAL Python теперь передан в этот ответ.

Ответ 4

В соответствии с принятым ответом и комментариями, это то, что я закончил:

file: setup.py

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

file: __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 запрашивается

  • добавлен дополнительный импорт (для упрощенного интерфейса пакета) если проект действительно установлен

Ответ 5

Поместите __version__ в your_pkg/__init__.py и проанализируйте в setup.py с помощью ast:

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 недоступен. Более того, любой backport importlib, конечно, нельзя полагаться на доступ к setup.py. В этом случае используйте:

import os

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