Каков правильный способ поделиться версией пакета 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 а посылка?

7 54

7 ответов:

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

file:setup.py

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

file:__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

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

папка 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.
)

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

Я согласен с @Стефано-м 'ы философии о:

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

и этот ответ получен из @zero-piraeus ' s ответ. Все дело в том, что " не используйте импорт в setup.py вместо этого прочитайте версию из a папка."

Я использую регулярное выражение для разбора __version__ так что это не должно быть последней строкой выделенного файла вообще. На самом деле, я все еще ставлю единственный источник истины __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.
)

... который все еще не идеален ... но это завод.

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

python setup.py --version
1.2.3

PS: Это официальный упаковочный документ Pythonзеркала) описывает несколько вариантов. Его первый вариант также использует регулярное выражение. (В зависимости от точного регулярного выражения, которое вы используете, оно может обрабатывать или не обрабатывать кавычки внутри строки версии. Вообще не большая проблема, хотя.)

ППС: о исправить в Adal Python теперь backported в этот ответ.

поставить __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 importlib.util.find_spec не имеется. Кроме того, каких-либо портировать из importlib конечно, нельзя полагаться на доступность setup.py. В этом случае используйте:

import os

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

есть несколько методов, предлагаемых в направляющие для упаковки on python.org.

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

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 is просил

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

принятый ответ требует, чтобы пакет был установлен. В моем случае мне нужно было извлечь параметры установки (в том числе __version__) из источника setup.py. Я нашел прямое и простое решение, просматривая тесты пакета setuptools. В поисках дополнительной информации о _setup_stop_after атрибут приведет меня к старый список рассылки пост котором упоминается distutils.core.run_setup, что привело меня к фактические документы нужны. После всего этого, вот простое решение:

file setup.py:

from setuptools import setup

setup(name='funniest',
      version='0.1',
      description='The funniest joke in the world',
      url='http://github.com/storborg/funniest',
      author='Flying Circus',
      author_email='flyingcircus@example.com',
      license='MIT',
      packages=['funniest'],
      zip_safe=False)

file extract.py:

from distutils.core import run_setup
dist = run_setup('./setup.py', stop_after='init')
dist.get_version()