Как задать версию пакета с помощью __init__.py
Файл __init__.py и версионирование пакетов
Как эффективно задать версию пакета в __init__.py и обеспечить её доступность как внутри пакета, так и для внешних инструментов?
Наиболее эффективное решение сочетает ручное определение атрибута __version__ в файле __init__.py с возможностью получения версии из метаданных установленного пакета через importlib.metadata. Такой подход даёт единую точку версии во время разработки и корректное чтение версии после установки пакета.
Пример реализации:
# mypackage/__init__.py
__version__ = '1.2.3'
# Для получения версии после установки пакета (например, в другом модуле)
from importlib.metadata import version
def get_package_version():
try:
return version('mypackage')
except:
return __version__
Python init py version (файл __init__.py и версионирование)
>>> import mypackage >>> mypackage.__version__ '1.2.3' >>> mypackage.get_package_version() '1.2.3'
Цель: обеспечить доступ к версии пакета без дополнительной установки (через атрибут __version__) и при установленном пакете (через метаданные, которые обновляются автоматически при публикации). Случай использования: разработка пакета с открытым исходным кодом, где требуется версионирование для CI/CD и для пользователей.
Типичная ошибка: несоответствие значения __version__ в __init__.py и версии, указанной в setup.cfg или pyproject.toml. При публикации на PyPI версия из метаданных становится приоритетной, и если они различаются, пользователи могут получить неактуальную информацию. Решение: использовать единый источник версии (например, динамическое чтение из __init__.py при сборке или инструменты автоматической синхронизации).
Как получить версию только из метаданных, не дублируя её в коде?
Можно полностью отказаться от атрибута __version__ и полагаться только на importlib.metadata. Это устраняет дублирование, но версия становится доступна только после установки пакета. В процессе разработки (при запуске из исходного кода) получить версию таким способом не удастся.
# mypackage/__init__.py
# без __version__
# использование в другом месте
from importlib.metadata import version
print(version('mypackage'))
'1.2.3'
Цель: минимизация дублирования и ошибок синхронизации. Случай: пакеты, которые никогда не используются в режиме разработки (например, конечные приложения).
Как хранить версию в отдельном файле _version.py и импортировать её в __init__.py?
Отделение версии в собственный модуль упрощает автоматическую генерацию (например, с помощью скриптов или инструментов вроде versioneer).
# mypackage/_version.py
__version__ = '2.0.0'
# mypackage/__init__.py
from ._version import __version__
# теперь __version__ доступен как mypackage.__version__
Цель: изоляция версии для автоматического обновления. Случай: использование versioneer или setuptools-scm, которые генерируют _version.py на основе тегов git.
Как автоматически генерировать версию из системы контроля версий (Git)?
Инструменты setuptools-scm и versioneer позволяют вычислять версию на основе Git-тегов и коммитов. Они создают (или обновляют) файл _version.py или добавляют атрибут в __init__.py.
# pyproject.toml с setuptools-scm
[build-system]
requires = ["setuptools-scm"]
build-backend = "setuptools.build_meta"
[project]
name = "mypackage"
dynamic = ["version"]
[tool.setuptools_scm]
write_to = "mypackage/_version.py"
После сборки в mypackage/_version.py будет сгенерирована строка версии. В __init__.py делается импорт:
from ._version import __version__
Цель: полностью автоматическое версионирование без ручного обновления. Случай: проекты с активной разработкой на Git и регулярными релизами.
Как использовать старый метод pkg_resources для получения версии?
До появления importlib.metadata часто применялся pkg_resources из setuptools. Он медленнее и считается устаревшим, но иногда встречается в легаси-проектах.
import pkg_resources
version = pkg_resources.get_distribution('mypackage').version
Цель: совместимость со старым кодом. Случай: поддержка пакетов, написанных до Python 3.8.
Как хранить версию в текстовом файле (например, VERSION) и читать его в __init__.py?
Некоторые проекты помещают версию в отдельный файл VERSION или version.txt и читают его при импорте. Это позволяет другим инструментам (например, Makefile) легко получить версию.
# VERSION
3.1.4
# mypackage/__init__.py
import os
with open(os.path.join(os.path.dirname(__file__), '..', 'VERSION')) as f:
__version__ = f.read().strip()
Цель: централизованное хранение версии вне Python-кода. Случай: проекты, где версия нужна не только Python-приложениям (например, для скриптов сборки).
Проблема при использовании файла VERSION: если пакет установлен, относительный путь '..' может не указывать на директорию с файлом VERSION. Это приведет к ошибке FileNotFoundError. Решение: использовать importlib.resources для чтения файла внутри пакета, либо копировать VERSION в пакет при сборке.
Расширенные примеры версионирования
Иерархическое версионирование подпакетов
В больших пакетах можно задавать версию для каждого подпакета отдельно, экспортируя её через __init__.py.
# mypackage/subpackage1/__init__.py
__version__ = '1.0.0'
# mypackage/subpackage2/__init__.py
__version__ = '2.0.0'
# Использование
import mypackage.subpackage1
print(mypackage.subpackage1.__version__) # '1.0.0'
Такая структура полезна, если подпакеты развиваются с разной скоростью. Однако следует документировать, что версия корневого пакета (например, mypackage.__version__) может не совпадать с версиями вложенных модулей.
Автоматическое обновление версии с помощью versioneer
versioneer создаёт файл _version.py на основе Git-тегов и может внедрять переменную в __init__.py. Полная настройка включает:
# Установка и инициализация
pip install versioneer
versioneer install
После этого в setup.cfg добавляется:
[versioneer]
VCS = git
style = pep440
versionfile_source = mypackage/_version.py
versionfile_build = mypackage/_version.py
tag_prefix = v
parentdir_prefix = mypackage-
В __init__.py:
from ._version import get_versions
__version__ = get_versions()['version']
del get_versions
Результат при наличии тега v1.2.0:
>>> import mypackage >>> mypackage.__version__ '1.2.0'
При отсутствии тега версия будет содержать хеш коммита.
Использование __all__ вместе с __version__
В __init__.py можно экспортировать не только версию, но и список публичных имён. Это помогает инструментам статического анализа.
# mypackage/__init__.py
__version__ = '0.5.0'
__all__ = ['submodule1', 'submodule2', '__version__']
from . import submodule1, submodule2
Теперь from mypackage import * импортирует и __version__, и подмодули.
Динамическое получение версии из git в рантайме (без файла)
Некоторые проекты отказываются от хранения версии в файле и получают её напрямую из Git при запуске. Это не рекомендуется для установленных пакетов, но возможно в среде разработки.
import subprocess
import os
def get_git_version():
try:
return subprocess.check_output(
['git', 'describe', '--tags'],
cwd=os.path.dirname(__file__)
).decode().strip()
except Exception:
return 'unknown'
__version__ = get_git_version()
Результат:
>>> import mypackage >>> mypackage.__version__ 'v1.0.0-5-gabcdef'
Недостаток: высокая задержка при первом импорте, зависимость от Git и отсутствие версии после установки (если репозиторий не включён).
Кэширование версии при импорте
Если получение версии затратно (например, вызов Git), можно сохранить результат в глобальную переменную.
# mypackage/__init__.py
_version = None
def _get_version():
global _version
if _version is None:
# вычисляем один раз
from importlib.metadata import version
try:
_version = version('mypackage')
except:
_version = '0.0.0.dev'
return _version
__version__ = _get_version()
Такой подход позволяет ускорить повторные обращения к атрибуту.