Руководство по pyproject.toml: упаковка, зависимости и инструменты
Автоматический перевод Эта статья была автоматически переведена с оригинальной английской версии.
В старых Python-проектах конфигурация часто была распределена между setup.py, setup.cfg, файлами requirements, MANIFEST.in и отдельными файлами для инструментов разработки. pyproject.toml объединяет эти задачи в одном месте, хотя не заменяет каждый файл проекта и не превращает все секции в части единого стандарта.
В этом руководстве мы разберём файл — от конфигурации сборки до метаданных проекта и настроек инструментов. Четыре владельца помогают разделять секции: что собирает проект, что проект публикует, что локально нужно контрибьюторам и какие параметры настраивают отдельные инструменты.
Кратко. Используйте
[build-system]для бэкенда, который собирает дистрибутив,[project]для публикуемых метаданных и runtime-требований,[dependency-groups]для непубликуемых окружений разработки, а[tool.*]— только если это предусмотрено документацией соответствующего инструмента. Декларация зависимостей — не lockfile.
Один файл, четыре владельца
Структуру ядра определяют три стандарта упаковки:
- PEP 518 определяет требования системы сборки.
- PEP 621 определяет метаданные проекта.
- PEP 735 определяет непубликуемые группы зависимостей.
Авторы инструментов также могут объявлять namespace внутри [tool]. Содержимое этого namespace не стандартизировано: каждый инструмент сам определяет свои ключи и поведение.
Вот небольшой упакованный пакет-библиотека:
[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"
Каждый список зависимостей отвечает на свой вопрос.
[build-system]: как исходный код превращается в дистрибутив
Build frontend, например python -m build, pip или uv, вызывает build backend. Бэкенд определяет, как дерево исходников превращается в sdist или wheel и какие файлы попадут в эти артефакты.
[build-system]
requires = ["hatchling>=1.26"]
build-backend = "hatchling.build"
requires содержит зависимости, необходимые для запуска бэкенда в изолированном build-окружении. Это не список runtime-зависимостей вашего пакета.
Объявляйте систему сборки, если проект выпускает дистрибутив или требует установки собственного кода как пакета. Непакетный проект тоже может использовать pyproject.toml; PEP 735 даже допускает файл, содержащий только группы зависимостей. Менеджеры окружений по-разному работают с проектами без системы сборки, поэтому принимайте это решение осознанно.
Выбирайте бэкенд с учётом требований сборки:
- layout на чистом Python и требований к выбору файлов
- компилируемые расширения или внешние системы сборки
- динамическая версия или требования к генерируемым файлам
- поведение при editable-установке
- зрелость бэкенда в release pipeline
Скопируйте актуальную рекомендуемую таблицу бэкенда из его документации. Не добавляйте wheel в требования сборки по привычке: бэкенд сам объявляет, что ему нужно.
[project]: какие метаданные получают потребители
Таблица [project] описывает дистрибутив: его имя, версию, совместимость с Python, runtime-зависимости, entry points и другие метаданные для индексов пакетов.
[project]
name = "weather-client"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["httpx>=0.27"]
Спецификаторы зависимостей — это ограничения для резолвера, а не снапшот конкретного окружения. Они становятся частью метаданных wheel и sdist, поэтому downstream-инсталляторы могут объединять их с требованиями других пакетов.
Extras — это публичный интерфейс установки
[project.optional-dependencies] определяет extras, которые могут запросить потребители:
[project.optional-dependencies]
cli = ["rich>=13"]
postgres = ["psycopg[binary]>=3.2"]
Потребитель может установить weather-client[cli]. Поскольку имена extras и требования публикуются, относитесь к ним как к возможностям продукта. Не используйте extra с именем dev просто для хранения всех инструментов контрибьютора.
Entry points связывают установленные команды с Python
[project.scripts]
weather = "weather_client.cli:main"
После установки дистрибутива окружение предоставляет weather, который импортирует и вызывает weather_client.cli:main. Тестируйте команду из собранного 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" },
]
Это подходящая граница для тестов, линтеров, сборщиков документации и других инструментов контрибьютора. Группы также полезны для приложений или ноутбуков, которые не собирают дистрибутив.
Группы зависимостей — это стандартизированные данные, но интерфейсы инсталляторов всё ещё различаются. Проверьте, как выбранный менеджер окружений устанавливает, лочит и резолвит их. PEP 735 не определяет универсальный CLI-интерфейс.
[tool.*]: конфигурация, принадлежащая одному инструменту
Таблицы инструментов не используют общую схему:
[tool.pytest.ini_options]
addopts = "-ra"
testpaths = ["tests"]
[tool.ruff]
line-length = 100
Используйте таблицу [tool.*] только в том случае, если инструмент документирует её поддержку. Некоторые настройки по-прежнему должны находиться в отдельных файлах: например, если их потребляет другая экосистема, файлу нужен другой формат или отдельная конфигурация понятнее. pyproject.toml — это точка координации. Она не требует централизовать все настройки.
Декларации, lockfile и файлы requirements решают разные задачи
Частая причина путаницы — воспринимать каждый артефакт зависимостей как конкурирующий источник истины.
| Артефакт | Основное назначение | Типичное содержимое |
|---|---|---|
[project.dependencies] | Публичный runtime-контракт | Прямые требования и совместимые диапазоны |
[project.optional-dependencies] | Публикуемые возможности по запросу | Extras для потребителей |
[dependency-groups] | Непубликуемые локальные окружения | Группы для тестов, линтинга, документации или приложения |
| Lockfile | Воспроизведение разрешённого окружения | Точные версии, источники и метаданные разрешения |
requirements.txt | Входные данные для установки через pip | Требования и специфичные для pip параметры, constraints, URL или хэши |
Библиотека обычно публикует совместимые ограничения и тестируется на диапазоне версий. Приложение обычно коммитит lockfile менеджера окружений. Точные pin-версии должны находиться в разрешённом артефакте деплоя, а не бездумно в публичных метаданных библиотеки.
Сохраняйте файл requirements, если интеграции нужен формат или функциональность pip. Если проект под управлением uv требует такой файл, экспортируйте его из lockfile, а не поддерживайте два независимых набора зависимостей:
uv export --format requirements.txt --output-file requirements.txt
Экспорт — это производный compatibility-артефакт. Lockfile остаётся разрешённым источником для этого workflow.
Миграция с сохранением поведения
Не начинайте с удаления setup.py или requirements.txt. Сначала классифицируйте назначение каждого существующего файла.
- Проведите инвентаризацию логики сборки, метаданных, runtime-требований, extras, окружений разработчика, настроек инструментов, данных пакета и entry points.
- Выберите бэкенд, способный воспроизвести текущий выбор файлов, компилируемые артефакты и editable-установки.
- Перенесите статические публикуемые метаданные в
[project]; действительно динамические поля оставьте явно обозначенными. - Перенесите требования только для контрибьюторов в
[dependency-groups], а не в публичные extras. - Переносите настройки инструментов только в тех случаях, когда инструмент поддерживает эквивалентную семантику.
- Соберите и sdist, и wheel, проверьте их содержимое и установите wheel в чистое окружение.
- Запустите entry points, тесты, проверки импорта и фактический путь деплоя.
- Удаляйте старую конфигурацию только после того, как артефакты и поведение совпадут.
MANIFEST.in всё ещё может понадобиться для некоторых layout setuptools, а небольшой setup.py может оставаться допустимым для программного управления сборкой. Модернизация переносит ответственность между файлами. Удаление файлов — не цель.
Актуальный workflow с uv
При создании проекта uv различает приложения и библиотеки:
# Non-library application template
uv init weather-app
# Packaged library with a src layout and build system
uv init --lib weather-client
uv init создаёт файлы проекта. Первая операция с проектом, например uv run, uv sync или uv lock, при необходимости создаёт lockfile и постоянный .venv.
cd weather-client
uv add httpx
uv add --group test pytest pytest-cov
uv run pytest
uv build
Сейчас uv размещает локальные требования разработки в стандартизированных группах зависимостей. Встроенный бэкенд uv_build — один из вариантов для проектов на чистом Python; для компилируемых расширений нужен подходящий альтернативный бэкенд, например maturin или scikit-build-core. Актуальные настройки бэкенда см. в документации uv по конфигурации проекта.
Заключение
pyproject.toml остаётся понятным, когда у каждой таблицы есть одна аудитория. Изоляция сборки относится к [build-system], а публикуемое поведение — к [project]. Окружения контрибьюторов относятся к [dependency-groups]. Переключатели инструментов принадлежат инструменту, который их определяет.
Когда эти границы зафиксированы, файл становится проще проверять, а миграции перестают смешивать метаданные пакета с разрешённым окружением одной машины.