open atlas
↑ К треку
Python для JS/TS-разработчиков PY · 10 · 02

pyproject.toml и точки входа: бэкенды сборки, src-раскладка и шимы консольных скриптов

pyproject.toml [project] объявляет метаданные; build-бэкенд превращает исходники в wheel. src-раскладка заставляет тесты работать с установленным пакетом; editable-установка — это .pth-редирект; [project.scripts] генерирует шимы на PATH; плагины ищутся через группы entry points.

PY Senior ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Внутренний инструмент месяцами поставлялся с плоской раскладкой — mytool/ и tests/ рядом в корне репозитория. Потом платформенная команда завела тикет, похожий на загадку: установка инструмента ломала их тесты. Расследование: автообнаружение setuptools всё это время упаковывало корневой каталог tests/ в wheel, так что каждый pip install mytool тихо подкладывал в site-packages пакет с именем tests. Платформенная команда запускала свой набор с from tests.helpers import factory — и после установки инструмента import tests разрешался в чужие тестовые заглушки вместо их локального каталога, ровно в тех окружениях, где локальный путь проигрывал гонку sys.path. CI самого инструмента ничего не замечал: его тесты импортировали локальное дерево и никогда — wheel. Выкатили два фикса: раскладку src/, чтобы в корне репозитория не лежало ничего импортируемого, и шаг CI со списком содержимого wheel. Wheel был сломан 23 релиза подряд. Артефакт не тестировал никто — только исходники.

pyproject.toml: один файл, два контракта

Прежде чем трогать бэкенд сборки или разбираться, почему pip install делает именно это, задайте себе вопрос: какую из двух принципиально разных задач решает этот файл прямо сейчас? Ответ меняет то, какую таблицу редактировать и какой инструмент винить, когда что-то идёт не так.

В pyproject.toml два разделимых контракта. Таблица [project]метаданные: имя, версия, dependencies (диапазоны версий — это декларация библиотеки, а не лок-файл) и optional-dependencies (экстры вроде mytool[postgres], которые потребители включают сами). Таблица [build-system] отвечает на другой вопрос: кто превращает это дерево исходников в устанавливаемый артефакт. Названный бэкенд сборки — hatchling, setuptools, flit-core — это библиотека со стандартизованными хуками (build_wheel, build_sdist); фронтенды — pip, uv, python -m build — вызывают эти хуки в изолированном окружении с установленным requires. В этом разделении вся современная упаковка: pip не умеет собирать ваш проект, он умеет лишь спросить бэкенд. Бэкенды различаются конфигурацией и дефолтами, а не природой артефакта: у hatchling строгий, предсказуемый отбор файлов; setuptools несёт два десятилетия совместимости (и то самое автообнаружение, упаковавшее tests/ из Хука); flit-core минимален для чистого Python. Один по-настоящему сеньорский рычаг: dynamic = ["version"] плюс VCS-плагин (hatch-vcs, setuptools-scm) выводит версию из git-тега при сборке — один источник истины, никакого релизного коммита, бампающего строку в двух файлах и промахивающегося в одном.

src-раскладка: тестируйте то, что поставляете

Плоская раскладка кладёт импортируемый пакет в корень репозитория. Ловушка — арифметика sys.path из урока про импорты: запустите тесты из корня — и локальный каталог mytool/ выиграет каждый импорт, то есть тесты гоняют дерево исходников и никогда — собранный артефакт. Wheel без сабпакета, потерянный отбором файлов файл данных, протухшая точка входа — всё невидимо, потому что артефакт ни разу не на скамье подсудимых. Раскладка src/ уносит пакет в src/mytool/, которого нет на sys.path, — теперь import mytool может разрешиться только в установленный пакет, и прогон тестов проверяет ровно то, что произвёл pip install. Это и есть защита от «локально тесты зелёные, поехал сломанный wheel», и именно такую раскладку uv создаёт по умолчанию. Честная цена: перед тестами нужно ставить пакет (editable-установка делает это одноразовым шагом), а быстрые эксперименты python -c "import mytool" из корня репозитория перестают работать — что и требовалось.

Викторина

Ваш wheel уехал без mytool/templates/, прод упал с ModuleNotFoundError — а полный набор тестов был зелёным. Плоская раскладка, тесты гоняются из корня репозитория. Что ослепило тесты?

Editable-установка: .pth-редирект и его пределы

pip install -e . (или установка, которую uv sync выполняет для проекта воркспейса) не копирует ваш код. Бэкенд пишет в site-packages редирект — классически файл .pth (специальный файл-путь, который Python читает при старте и автоматически добавляет указанные пути к sys.path), добавляющий ваш src/ в sys.path, либо маленький импорт-хук для более тонкого контроля. Результат: окружение импортирует ваше рабочее дерево вживую; правка, перезапуск, без переустановки. Пределы прямо следуют из того, что не передиректили: метаданные — это снимок. Зависимости, точки входа, консольные скрипты — всё записано в момент установки. Добавьте новую запись в [project.scripts] или зависимость — editable-установка не узнает об этом до переустановки. Симптом всегда одной формы: «я добавил команду, а её нет на PATH» — код живой, метаданные заморожены.

Точки входа: консольные скрипты и обнаружение плагинов

[project.scripts] mycli = "mytool.cli:main" — метаданные, и действует по ним установщик: в момент установки он генерирует маленький исполняемый шим в каталоге bin/ окружения — на PATH, пока venv активен, — вся работа которого: from mytool.cli import main; sys.exit(main()). Ваш исходный файл никогда не попадает на PATH, ему не нужны ни chmod +x, ни шебанг; шим заодно объясняет, зачем была дисциплина -m из прошлого урока — шим импортирует ваш модуль с полным пакетным контекстом, а не исполняет файл. Возврат main становится кодом выхода.

Тот же механизм метаданных обобщается в плагинные системы. Любой дистрибутив может объявить точки входа под произвольным именем группы; любая программа может спросить importlib.metadata.entry_points(group="pytest11") и получить регистрации всех установленных дистрибутивов — чтение установленных метаданных, без сканирования файлов, без реестра, без конфигурации. Ровно так pytest находит плагины: поставьте pytest-cov — он появится в группе pytest11, pytest подгрузит его на старте; удалите — исчезнет. Две продакшен-заметки: стоимость обнаружения растёт с числом установленных дистрибутивов (дёшево, но не бесплатно в толстых окружениях), а загрузка точки входа импортирует модуль плагина — плагин с сайд-эффектами времени импорта наследует все режимы отказа прошлого урока, теперь срабатывающие на чужом старте.

Викторина

После pip install команда mycli работает в venv. Что на самом деле лежит на PATH?

Экстры против групп зависимостей

Если видите pytest в install_requires библиотеки — или экстру dev, тянущую линтеры в окружение каждого потребителя, — это именно та путаница, от которой существует этот раздел.

Два механизма, две аудитории, путают регулярно. Экстры (optional-dependencies) — публикуемые метаданные: mytool[postgres] — фича-флаг, который ставят ваши потребители; зависимости экстры едут в метаданных wheel на весь мир. Группы зависимостей ([dependency-groups], PEP 735) — сторона разработки: тест-раннеры, линтеры, тайп-чекеры — их разрешает ваш тулинг (uv по умолчанию ставит группу dev в воркспейс), и они никогда не публикуются в wheel. Отсюда дисциплина: pytest не место в экстре — экстра mytool[test] повезёт ваш тестовый стек каждому любопытному потребителю и загрязнит его разрешение версий; фичи живут в экстрах, рабочие процессы — в группах. Легаси-проекты имитируют группы экстрой dev, потому что старый тулинг старше PEP 735, — узнавайте в этом обходной манёвр, а не паттерн для копирования.

Вспомните перед уходом
  1. 01
    Что на самом деле делает бэкенд сборки и в чём аргумент src-раскладки — включая отказ, который она предотвращает, и цену, которую берёт?
  2. 02
    Проследите mycli от строки в pyproject до команды в шелле, затем объясните, как тот же механизм даёт pytest находить плагины, — и две продакшен-оговорки.
Итог

pyproject.toml несёт два контракта: [project] — публикуемые метаданные: имя, зависимости-диапазоны, экстры, которые потребители включают сами, — а [build-system] называет бэкенд, чьи стандартизованные хуки превращают дерево исходников в sdist и wheel по запросу фронтенда; pip и uv сами не собирают ничего, а dynamic-версия из VCS-тегов убирает релизный баг «бампни в двух файлах». Раскладка решает, что на самом деле тестируют ваши тесты: плоская оставляет пакет в корне репозитория, где sys.path разрешает его первым, — набор гоняет исходники, пока wheel гниёт невидимым; инструмент из Хука 23 релиза возил лишний пакет tests и сломал соседнюю команду прежде, чем кто-то заглянул в содержимое артефакта; src-раскладка снимает локальное дерево с гонки, прогоняя импорты через установленный пакет ценой шага установки. Editable-установка делает эту цену одноразовой через .pth-редирект — код живой, метаданные заморожены, так что новым точкам входа нужна переустановка. [project.scripts] превращает строку метаданных в сгенерированный шим в bin/, который импортирует вашу функцию и выходит с её возвратом; та же машинерия точек входа под именами групп — вся плагинная система pytest: объявлено в метаданных, обнаружено через importlib.metadata, импортировано при загрузке со всеми последствиями сайд-эффектов времени импорта. Экстры публикуют фичи; группы зависимостей держат dev-тулинг локально и подальше от резолверов ваших потребителей. Теперь, когда встретите отсутствие mycli после добавления скрипта, плагин, которого pytest не видит, или зависимость, просочившуюся в лок-файлы потребителей, вы знаете, какой слой проверить и за каким механизмом тянуться.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.