Каков правильный способ обмена версией пакета с помощью 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')
Ответ 6
Существует несколько методов, предложенных в Руководства по упаковке на python.org
.