Руководство по pyproject.toml: упаковка, зависимости и инструменты
Автоматический перевод
Эта статья была автоматически переведена с оригинальной английской версии.
В старых Python-проектах конфигурация часто разбросана по setup.py, setup.cfg, файлам requirements, MANIFEST.in и отдельным файлам инструментов разработки. pyproject.toml собирает все эти задачи в одном месте, хотя он не заменяет каждый файл проекта и не делает каждую секцию частью единого стандарта.
Руководство рассчитано на Python-разработчиков, которые поддерживают пакет или приложение. Вы узнаете, что хранит каждая из четырёх основных таблиц ([build-system], [project], [dependency-groups], [tool.*]), чем они отличаются от lock-файла и requirements.txt, как выбрать бэкенд сборки, как мигрировать старый проект и как настроить новый с помощью uv.
Один файл, четыре таблицы
Базовую структуру задали три стандарта упаковки:
- PEP 518 определяет требования к системе сборки.
- PEP 621 определяет метаданные проекта.
- PEP 735 определяет неопубликованные группы зависимостей.
Авторы инструментов могут также занять пространство имён внутри [tool]. Что в нём лежит, не стандартизировано: каждый инструмент сам определяет свои ключи и поведение.
На схеме пример файла из следующего раздела разбит на четыре таблицы и показано, какая программа читает каждую из них. Пользователей достигает только [project]: через метаданные в собранном wheel.
Вот небольшая упакованная библиотека:
[build-system]
requires = ["hatchling>=1.26"]
build-backend = "hatchling.build"
[project]
name = "weather-client"
version = "0.1.0"
description = "A small weather API client"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"httpx>=0.27",
]
[project.optional-dependencies]
cli = ["rich>=13"]
[project.scripts]
weather = "weather_client.cli:main"
[dependency-groups]
test = ["pytest>=8", "pytest-cov>=5"]
lint = ["ruff>=0.12"]
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.ruff]
line-length = 100
target-version = "py311"
В файле четыре списка зависимостей. dependencies перечисляет то, что пакету нужно во время выполнения, optional-dependencies перечисляет то, что пользователи могут подключить по желанию, [dependency-groups] перечисляет то, что контрибьюторы ставят локально, а [build-system].requires перечисляет то, что нужно бэкенду для сборки.
[build-system]: как исходники становятся дистрибутивом
Фронтенд сборки, например python -m build, pip или uv, вызывает бэкенд сборки. Бэкенд решает, как дерево исходников превращается в sdist или wheel и какие файлы попадают в эти артефакты. В примере build-backend указывает на hatchling, а requires перечисляет то, что фронтенд устанавливает для его запуска.
sdist — это архив исходников, который установщик собирает перед установкой; wheel содержит файлы, уже готовые к установке.
Сборка идёт в таком порядке. Фронтенд читает [build-system], устанавливает requires во временное окружение сборки и запускает бэкенд. Бэкенд читает [project] и записывает sdist и wheel.
requires содержит зависимости, нужные для запуска бэкенда в его изолированном окружении сборки. Это не список зависимостей вашего пакета во время выполнения.
Объявляйте систему сборки, если проект производит дистрибутив или его собственный код нужно устанавливать как пакет. Проект без пакета тоже может использовать pyproject.toml: PEP 735 даже допускает файл, где есть только группы зависимостей. Без системы сборки uv не устанавливает проект в его собственное окружение: импорт вашего кода работает только из корня репозитория, а команды из [project.scripts] uv не создаёт. Если нужно что-то из этого, задайте tool.uv.package = true или добавьте систему сборки (упаковка проектов в uv).
Выбор бэкенда
Выбирайте бэкенд по тому, что должна делать сборка:
| Бэкенд | Когда использовать |
|---|---|
uv_build | Проект на чистом Python, управляется через uv и не требует плагинов сборки. Его записывает uv init. |
hatchling | Проект на чистом Python, и ему нужны плагины сборки или версия из git (hatch-vcs). |
flit-core | Проект — самый простой пакет на чистом Python, плагины сборки не нужны. |
setuptools | Должны продолжать работать существующий setup.py, C-расширения или собственный шаг сборки. |
poetry-core | Команда управляет проектом через Poetry. |
maturin | Расширение написано на Rust. |
scikit-build-core | Расширение написано на C или C++ и собирается через CMake. |
meson-python | Проект собирается через Meson, как NumPy и SciPy. |
На схеме бэкенды расставлены от сборок, которые только копируют файлы Python, до сборок, которые компилируют код. Для версии из тегов Git используйте бэкенд с поддержкой такого способа версионирования, например hatchling с hatch-vcs или setuptools с setuptools-scm.
Копируйте актуальную рекомендуемую таблицу бэкенда из его документации. Не добавляйте wheel в требования сборки по привычке: бэкенд сам объявляет, что ему нужно.
[project]: метаданные для потребителей
Таблица [project] описывает дистрибутив: имя, версию, совместимость с версиями Python, зависимости времени выполнения, точки входа и прочие метаданные индекса.
Спецификаторы зависимостей, такие как httpx>=0.27 в примере, — это ограничения для резолвера, а не снимок одного окружения. Они становятся частью метаданных wheel и sdist, поэтому установщики ниже по цепочке могут объединять их с требованиями других пакетов.
Extras — публичный интерфейс установки
[project.optional-dependencies] определяет extras, которые могут запрашивать потребители. С extra cli из примера потребитель может установить weather-client[cli]. Имена extras и их требования публикуются, поэтому относитесь к ним как к возможностям продукта и называйте их соответственно, например cli или postgres. Не заводите extra с именем dev лишь для того, чтобы собрать в нём все инструменты контрибьюторов.
Точки входа связывают установленные команды с Python
В таблице [project.scripts] примера одна запись: weather = "weather_client.cli:main". После установки дистрибутива окружение предоставляет команду weather, которая импортирует weather_client.cli и вызывает main. Команда weather устанавливается и без extra cli, поэтому cli.py должен работать без rich или сообщать пользователю, что нужно установить weather-client[cli].
Проверяйте команду из собранного wheel, а не только из корня репозитория: пользователи получают именно wheel. Команды показаны в разделе о миграции.
[dependency-groups]: локальные неопубликованные окружения
Группы зависимостей из PEP 735 описывают окружения для разработки или проекты без пакета и не публикуются как метаданные пакета.
[dependency-groups]
test = ["pytest>=8", "pytest-cov>=5"]
lint = ["ruff>=0.12"]
dev = [
{ include-group = "test" },
{ include-group = "lint" },
]
Это подходящее место для тестов, линтеров, сборщиков документации и подобных инструментов контрибьюторов. Оно полезно и для приложений или ноутбуков, которые не собирают дистрибутив. include-group позволяет одной группе переиспользовать другую; у extras такой возможности нет.
PEP 735 не определяет интерфейс командной строки, поэтому у каждого установщика свои флаги. Группы и extras устанавливаются так:
# Dependency groups (not published)
pip install --group test # pip 25.1 and later
uv sync --group test
uv sync --all-groups
# Extras (published)
pip install "weather-client[cli]"
uv sync --extra cli
pyproject.toml объявляет зависимости, но не создаёт окружение. Команды, которые создают окружение и устанавливают проект вместе с группой, зависят от инструмента:
| Рабочий процесс | Создать окружение | Установить проект и группу test |
|---|---|---|
| pip | python -m venv .venv, затем активировать его | pip install -e . --group test |
| uv | uv sync создаёт .venv, если его нет | uv sync --group test (также устанавливает проект и dev) |
[tool.*]: конфигурация, которой владеет один инструмент
У таблиц инструментов нет общей схемы. В примере [tool.pytest.ini_options] и [tool.ruff] используют ключи, которые читает только их собственный инструмент. Используйте таблицу [tool.*] только если инструмент её документирует. Эти распространённые инструменты читают настройки из pyproject.toml:
| Задача | Инструмент | Таблица |
|---|---|---|
| Форматирование и линтинг | Ruff | [tool.ruff] |
| Тесты | pytest | [tool.pytest.ini_options] или [tool.pytest] |
| Проверка типов | mypy | [tool.mypy] |
| Покрытие | coverage.py | [tool.coverage.run], [tool.coverage.report] |
В pytest 9.0 появилась таблица [tool.pytest], которая использует нативные типы TOML, например список для addopts; более старые версии читают только [tool.pytest.ini_options].
Некоторые настройки по-прежнему лучше держать в собственных файлах: их читает другая программа или файлу нужен другой формат. flake8 не читает pyproject.toml; он читает .flake8, setup.cfg или tox.ini. pre-commit читает .pre-commit-config.yaml.
Объявления, lock-файлы и файлы requirements решают разные задачи
На схеме показаны пакеты из примера файла. Каждый список попадает в своё окружение, а пользователи получают только два списка из [project].
Информация о зависимостях хранится в пяти местах. У каждого своя задача:
| Артефакт | Основное назначение | Типичное содержимое |
|---|---|---|
[project.dependencies] | Публикуемый контракт времени выполнения | Прямые требования и совместимые диапазоны |
[project.optional-dependencies] | Публикуемые опциональные возможности | Extras для потребителей |
[dependency-groups] | Неопубликованные локальные окружения | Группы для тестов, линтинга, документации или приложения |
| Lock-файл | Воспроизвести разрешённое окружение | Точные версии, источники и метаданные разрешения |
requirements.txt | Входные данные установки для pip | Требования плюс специфичные для pip опции, ограничения, URL или хеши |
Библиотека обычно публикует совместимые ограничения и тестируется на диапазоне версий. Приложение обычно коммитит lock-файл своего менеджера окружений. Точные версии относятся к разрешённому артефакту деплоя, а не бездумно к публичным метаданным библиотеки. Например, библиотека объявляет httpx>=0.27, а версию выбирает резолвер пользователя. Приложение коммитит uv.lock и устанавливает его командой uv sync --locked.
Оставляйте файл requirements, когда интеграция требует формата или возможностей pip. Если проекту под управлением uv он нужен, экспортируйте его из lock-файла, а не поддерживайте два независимых набора зависимостей:
uv export --format requirements.txt --output-file requirements.txt
Экспорт — это производный вывод для совместимости. Lock-файл остаётся источником разрешённых версий для этого процесса.
Миграция, которая сохраняет поведение
Не начинайте с удаления setup.py или requirements.txt. Сначала классифицируйте, что делает каждый существующий файл. На схеме показано, куда в pyproject.toml переезжает каждая часть старой настройки и какие файлы остаются на месте.
- Составьте опись: логика сборки, метаданные, зависимости времени выполнения, extras, окружения разработчиков, настройки инструментов, данные пакета и точки входа.
- Выберите бэкенд, который воспроизводит текущее включение файлов, скомпилированные артефакты и editable-установки.
- Перенесите статические публикуемые метаданные в
[project]; по-настоящему динамические поля оставьте явными. - Перенесите требования, нужные только контрибьюторам, в
[dependency-groups], а не в публичные extras. - Переносите настройки инструментов только там, где инструмент поддерживает эквивалентную семантику.
- Соберите и sdist, и wheel, проверьте их содержимое и установите wheel в чистое окружение.
- Запустите точки входа, тесты, проверки импорта и реальный путь деплоя.
- Удаляйте старую конфигурацию только после того, как артефакты и поведение совпали.
Шаги 6 и 7 для примера библиотеки выглядят так:
uv build # or: python -m build
unzip -l dist/*.whl # files in the wheel
tar -tzf dist/*.tar.gz # files in the sdist
uv venv /tmp/check
uv pip install --python /tmp/check dist/*.whl
/tmp/check/bin/weather --help
Сравните оба списка файлов со старой сборкой, прежде чем что-то удалять. Пропавший файл данных или модуль обнаружится здесь, а не в рабочей копии репозитория.
MANIFEST.in может по-прежнему понадобиться при некоторых раскладках setuptools, а небольшой setup.py может остаться допустимым для программного поведения сборки.
Актуальный процесс работы с uv
uv init создаёт разную раскладку для каждого флага. Выберите один из этих вариантов:
# Packaged application: src/weather_app/, uv_build, and a weather-app command
uv init weather-app
# Single main.py and no build system
uv init --no-package weather-app
# Library: src layout, uv_build, and a py.typed marker
uv init --lib weather-client
В uv 0.12.0 упакованная раскладка стала раскладкой по умолчанию. Более ранние версии по умолчанию создавали раскладку с main.py.
uv init создаёт файлы проекта. uv lock создаёт или обновляет uv.lock, не создавая .venv. uv sync или uv run обновляет lock-файл, а также создаёт и синхронизирует .venv по мере необходимости.
Продолжите с библиотекой, созданной командой uv init --lib weather-client. Она содержит сгенерированную функцию hello(), поэтому перед запуском pytest добавьте небольшой тест:
cd weather-client
uv add httpx
uv add --group test pytest pytest-cov
mkdir -p tests
cat > tests/test_weather_client.py <<'PY'
from weather_client import hello
def test_hello():
assert hello() == "Hello from weather-client!"
PY
uv run --group test pytest
uv build
Тест должен сообщить 1 passed. --group test выбирает тестовые зависимости даже после того, как обычный uv sync их удалил.
Перед публикацией проверьте архивы и установите wheel в чистое окружение, как показано в разделе о миграции. В этой сгенерированной библиотеке нет команды weather, поэтому проверьте import weather_client. Когда всё готово, загрузите файлы из dist/ в PyPI:
uv publish
Вне uv то же самое делает python -m build с последующим twine upload dist/*.
uv помещает локальные требования для разработки в стандартизированные группы зависимостей. Его нативный бэкенд uv_build поддерживает только проекты на чистом Python; для компилируемых расширений нужен другой бэкенд из таблицы выше, например maturin или scikit-build-core. Актуальные настройки бэкенда смотрите в документации uv по конфигурации проекта.
Несколько пакетов в одном репозитории
Репозиторий с несколькими пакетами можно оформить как один workspace uv. Корневой pyproject.toml перечисляет участников:
[tool.uv.workspace]
members = ["packages/*"]
Участники делят один uv.lock и один .venv. uv run --package <name> запускает команду в контексте одного участника.