Руководство по 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.

Пример pyproject.toml, разбитый на четыре таблицы: сборочный фронтенд читает build-system, hatchling превращает project в метаданные wheel, которые pip устанавливает пользователю, uv sync устанавливает группу test в .venv, а pytest и ruff читают свои собственные таблицыПример pyproject.toml, разбитый на четыре таблицы: сборочный фронтенд читает build-system, hatchling превращает project в метаданные wheel, которые pip устанавливает пользователю, uv sync устанавливает группу test в .venv, а pytest и ruff читают свои собственные таблицы

Вот небольшая упакованная библиотека:

[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.

Бэкенды сборки в четырёх колонках: от чистого Python без плагинов (uv_build, flit-core) до чистого Python с плагинами (hatchling), старой сборки через setup.py (setuptools) и скомпилированных расширений (maturin, scikit-build-core, meson-python)Бэкенды сборки в четырёх колонках: от чистого Python без плагинов (uv_build, flit-core) до чистого Python с плагинами (hatchling), старой сборки через setup.py (setuptools) и скомпилированных расширений (maturin, scikit-build-core, meson-python)

Копируйте актуальную рекомендуемую таблицу бэкенда из его документации. Не добавляйте 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
pippython -m venv .venv, затем активировать егоpip install -e . --group test
uvuv 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].

Сетка из четырёх списков зависимостей и трёх окружений: hatchling только во временном окружении сборки, httpx у пользователей и контрибьюторов, rich только у пользователей с extra cli, а pytest и pytest-cov только в .venv контрибьютораСетка из четырёх списков зависимостей и трёх окружений: hatchling только во временном окружении сборки, httpx у пользователей и контрибьюторов, rich только у пользователей с extra cli, а pytest и pytest-cov только в .venv контрибьютора

Информация о зависимостях хранится в пяти местах. У каждого своя задача:

АртефактОсновное назначениеТипичное содержимое
[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 переезжает каждая часть старой настройки и какие файлы остаются на месте.

Старые файлы и их новые места: метаданные из setup.py, install_requires, extras_require и точки входа попадают в таблицы project, требования для разработки идут в dependency-groups, pytest.ini переезжает в таблицу tool, а небольшой setup.py, MANIFEST.in, .flake8 и requirements.txt, экспортированный из uv.lock, остаются снаружиСтарые файлы и их новые места: метаданные из setup.py, install_requires, extras_require и точки входа попадают в таблицы project, требования для разработки идут в dependency-groups, pytest.ini переезжает в таблицу tool, а небольшой setup.py, MANIFEST.in, .flake8 и requirements.txt, экспортированный из uv.lock, остаются снаружи

  1. Составьте опись: логика сборки, метаданные, зависимости времени выполнения, extras, окружения разработчиков, настройки инструментов, данные пакета и точки входа.
  2. Выберите бэкенд, который воспроизводит текущее включение файлов, скомпилированные артефакты и editable-установки.
  3. Перенесите статические публикуемые метаданные в [project]; по-настоящему динамические поля оставьте явными.
  4. Перенесите требования, нужные только контрибьюторам, в [dependency-groups], а не в публичные extras.
  5. Переносите настройки инструментов только там, где инструмент поддерживает эквивалентную семантику.
  6. Соберите и sdist, и wheel, проверьте их содержимое и установите wheel в чистое окружение.
  7. Запустите точки входа, тесты, проверки импорта и реальный путь деплоя.
  8. Удаляйте старую конфигурацию только после того, как артефакты и поведение совпали.

Шаги 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> запускает команду в контексте одного участника.

Ссылки