На предыдущем шаге каждая новая установка зависимостей приводила сначала к созданию, а потом и обновлению lock-файла.
Обсудим этот файл подробнее. Как мы уже обсуждали, в файле pyproject.toml указываются зависимости. При этом у каждой зависимости могут быть свои собственные зависимости, которые также обновляются и так до бесконечности.
Зависимости зависимостей называются транзитивными, и с ними все не просто. Система зависимостей может быть очень запутанной. Для такой ситуации придумали специальный термин — «ад зависимостей» или dependency hell.
Проблема заключается в том, что мы никак не фиксируем версии транзитивных зависимостей. Представим такой пример:
- В нашем проекте есть зависимый пакет A с зафиксированной версией
1.3.2
- У зависимости А есть зависимый пакет B с версией
*
uv
предоставляет команду uv sync
, которая синхронизирует зависимости в проекте с теми, что указаны в pyproject.toml. Так без lock-файла команда uv sync
поставила бы:
- Для A — указанную версию
- Для B — последнюю доступную версию из репозитория
Другими словами, выбор версии не детерминирован. Если автор обновит B и нарушит обратную совместимость, то пакет А перестанет работать — весь проект просто сломается.
Можно вручную отслеживать зависимости всех зависимостей и явно прописывать их версии в pyproject.toml. Но такой способ вряд ли сработает, потому что пакеты постоянно обновляются и меняются. Еще отслеживать вручную сложно, потому что связей слишком много — даже в проекте с пятью зависимостями будут сотни транзитивных зависимостей.
Другой выход — требовать, чтобы создатели всех библиотек всегда указывали версии. Этот вариант тоже не сработает, на этот раз из-за человеческого фактора.
Есть одно решение, которое точно сработает — это lock-файл. По сути это автоматизированное отслеживание зависимостей. Содержимое lock-файла выглядит примерно так:
version = 1
requires-python = ">=3.13"
[[package]]
name = "hexlet-hello-world"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "pytest" },
]
[package.metadata]
requires-dist = [{ name = "pytest", specifier = ">=8.3.3" }]
[[package]]
name = "pytest"
version = "8.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
]
sdist = { url = "https://files.pythonhosted.org/-/pytest-8.3.3.tar.gz", hash = "sha256:70b98", size = 1442487 }
wheels = [
{ url = "https://files.pythonhosted.org/-/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 },
]
[[package]]
name = "packaging"
version = "24.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/-/packaging-24.2.tar.gz", hash = "sha256:c228a..", size = 163950 }
wheels = [
{ url = "https://files.pythonhosted.org/-/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb..", size = 65451 },
]
[[package]]
name = "pluggy"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/-/pluggy-1.5.0.tar.gz", hash = "sha256:2cff..", size = 67955 }
wheels = [
{ url = "https://files.pythonhosted.org/-/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1..", size = 20556 },
]
# мы не приводим весь локфайл, т.к. его содержимое довольно большое
...
Первый запуск установки зависимостей сформирует этот файл. Туда запишутся все установленные зависимости, в том числе транзитивные с версиями и хеш-суммами.
Так в примере выше, пакет pytest
зависит от пакетов colorama
, iniconfig
, packaging
и pluggy
. И дальше в лок-файле записана информация об этих пакетах.
При дальнейших запусках команда uv sync
всегда ставит то, что указано в lock-файле. Это сработает, даже если удалить папку .venv или добавить новые версии пакетов в файл pyproject.toml. Повторный запуск через любой промежуток времени приведет к тому же результату. Теперь с уверенностью можно сказать — проект запустится в любое время и для любого пользователя.
Версионирование
Поговорим об обновлении зависимостей. Для обновления всех зависимостей нужно выполнить команду uv sync --upgrade
или кратко uv sync -U
. Чтобы выполнить обновление конкретной зависимости — uv add -U name
, где name
— имя библиотеки. А как будет происходить обновление, зависит от того, что написано в pyproject.toml
.
Рассмотрим все доступные варианты:
dependencies = [
"package1",
"package2 == 1.3.5",
"package3 ~= 2.10.3",
"package4 >= 3.0, < 4.0"
]
Вариант без указания версии означает, что можно ставить любую версию библиотеки. После выполнения команды обновления в папке .venv
окажется последняя доступная версия package
Если указан конкретный номер через ==
, то версия библиотеки жестко зафиксирована и никакая команда не сможет обновить ее
Самый интересный сценарий происходит в случае использования тильды (~
).
В семантическом версионировании считается, что patch, последняя цифра в версии, изменяется только в случае исправления ошибок, значит, обратная совместимость не должна теряться. При этом на практике это не всегда так. Код может работать с учетом ошибок в зависимостях.
Как правило, в проектах десятки, а то и сотни зависимостей, причем обновляются они часто. Казалось бы, тогда можно не указывать версию, но тогда обновления нередко могут ломать систему из-за мажорных обновлений библиотек. С другой стороны, можно зафиксировать все версии, и тогда обновлять все придется вручную.
Поэтому появился третий вариант. Добавление тильды приводит к тому, что в автоматическом режиме обновляются только патчи. Предположим что после добавления зависимости в проект, версия была установлена в ~2.10.3
. Если после нее разработчики выпустили 2.10.5
, то она будет установлена командой обновления.
То же самое произойдет, если потом будет выпущена версия 2.10.15
. Но если создатель библиотеки опубликует изменения в мажорной и минорной версии, например, 2.11.5
или 3.0.0
, то менеджер зависимостей их проигнорирует.
Примерно то же самое происходит и при использовании сочетания >= , <
. Здесь фиксируется только мажорная версия, а минорную и патч разрешено обновлять.
Самостоятельная работа
- Клонируйте репозиторий python-package, а затем выполните внутри него команду
uv sync -U
. Изучите вывод командыgit diff
- Попробуйте изменить версию любого пакета в
pyproject.toml
, например указать фиксированную версию, и выполнитьuv sync
. Доступные версии пакета можно посмотреть на сайте официального индекса пакетов PyPi, найдите нужный пакет и перейдите во вкладку History.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.