PEP 723 + uv: однофайловые скрипты с зависимостями

от автора

Привет, Хабр!

Если вы собираете прототип на C++, то один файл с main.cpp иногда реально компилируется в рабочую утилиту. Библиотеки либо завозятся пакетным менеджером заранее, либо у вас есть header‑only зависимость и всё взлетает. В Python долгое время это было болью: любой однофайловый скрипт, который требует requests или rich, уже тянет за собой виртуальные окружения, инструкции в README и локальные фичи.

Есть рабочий стандарт для нормальных однофайловых сценариев с зависимостями — PEP 723: вы объявляете зависимости прямо в комментариях, а раннер ставит всё сам и запускает в изолированной среде. В связке с uv получается неплохой такой способ делиться скриптами, в том числе для пвспомогательных задач. И да, у этой красоты есть нюансы безопасности, о них поговорим отдельно.

Что такое PEP 723 и как это выглядит

PEP 723 определяет блок метаданных в комментариях, который парсится внешними инструментами. Формат прост: сверху и снизу маркеры, внутри TOML c полями dependencies и requires-python.

Пример минимального скрипта:

# /// script # requires-python = ">=3.11" # dependencies = [ #   "httpx<1.0", #   "rich>=13.7", # ] # /// import httpx from rich import print  def main() -> None:     r = httpx.get("https://httpbin.org/json", timeout=10)     r.raise_for_status()     print({"status": r.status_code, "len": len(r.text)})  if __name__ == "__main__":     main()

Запуск через uv:

uv run example.py

uv создаст изолированную среду в кеше, установит зависимости и выполнит код. Никаких ручных venv.

Добавляем и управляем зависимостями прямо из CLI

PEP 723 — это про формат, но править TOML руками быстро надоедает. uv умеет редактировать блок зависимостей в файле:

uv add --script example.py "pydantic>=2.8" uv remove --script example.py "httpx" uv run example.py

Если нужен исполняемый файл без явного вызова uv run, используем shebang:

#!/usr/bin/env -S uv run --script # /// script # dependencies = ["rich"] # /// from rich import print print("hello")

Ставим права и запускаем из каталога:

chmod +x greet ./greet

Лочим зависимости и делаем скрипт воспроизводимым

Одно дело установить актуальные версии, другое — зафиксировать их. uv поддерживает lock‑файл для скриптов.

uv lock --script example.py # появится example.py.lock  # теперь любые операции будут учитывать lock: uv run --script example.py uv add --script example.py "rich"      # обновит lock uv export --script example.py -o req.txt

Можно ограничить свежесть пакетов датой, чтобы избежать неожиданных апдейтов через год. В блоке [tool.uv] внутри метаданных:

# /// script # requires-python = ">=3.11" # dependencies = ["httpx<1.0"] # [tool.uv] # exclude-newer = "2025-08-01T00:00:00Z" # ///

exclude-newer заставит резолвер игнорировать релизы, вышедшие после указанной даты.

А если нужно именно pip‑совместимое зафиксированное дерево для длительных CI‑пайплайнов, у uv есть инструменты уровня pip-tools: uv pip compile и uv pip sync. Они позволяют сгенерировать requirements.txt из исходника‑декларации и синхронизировать окружение один‑в‑один.

Контроль версии Python на рантайме

Скрипт может требовать конкретную ветку интерпретатора. В этом случае uv позволяет запросить нужную версию при запуске:

uv run --python 3.10 example.py uv run --python 3.12 example.py

Безопасность

Однофайловые скрипты удобно запускать как есть, и именно поэтому надо быть аккуратнее обычного. Риски:

  1. Подмена зависимостей. Тривиальные атаки на цепочку поставок через зарегистрированные пакеты с похожими именами.

  2. Непредсказуемые апгрейды. Сегодня всё чисто, завтра новый релиз транзитивной зависимости ломает инварианты.

  3. Источники пакетов. Переопределение индексов, отключение TLS‑проверок, внутренние зеркала без политики.

  4. Уязвимости в уже выбранных версиях.

Конкретные меры:

Фиксация и аудит. Для скриптов создаём example.py.lock и регулярно прогоняем аудит зависимостей. В экосистеме PyPA для этого есть pip-audit, он использует базу уязвимостей PyPI и может работать поверх вашего requirements или установленной среды. Можно запускать через uvx в изолированном окружении:

uvx pip-audit -r req.txt # или просканировать локальную среду uvx pip-audit --local

Проект поддерживается PyPA, есть GitHub Action.

Хеши и синхронизация. Для больших пайплайнов можно генерировать requirements с хешами, а устанавливать через uv pip sync, чтобы окружение соответствовало файлу один к одному. Управление хешами и параметры компиляции конфигурируются в uv.toml/pyproject.toml для uv pip compile.

Индексы и TLS. Не используем доверенные небезопасные хосты. В справочнике CLI прямо есть предупреждение: флаг --allow-insecure-host отключает верификацию цепочки сертификатов, что делает вас уязвимыми для MITM.

Воспроизводимость по времени. Добавляем exclude-newer к скрипту, держите lock рядом с ним, а CI запускайте с uv run --script или с экспортом в requirements.txt и последующим uv pip sync.

Сценарий: маленький CLI-интеграционный скрипт

Условие: нужен однофайловый инструмент для выгрузки данных из API, с ретраями и логами, без установки проекта и с воспроизводимыми версиями.

fetch_users.py:

# /// script # requires-python = ">=3.11" # dependencies = [ #   "httpx==0.27.2", #   "tenacity==9.0.0", #   "structlog==24.4.0", # ] # [tool.uv] # exclude-newer = "2025-08-01T00:00:00Z" # /// from __future__ import annotations  import os import sys import json import time import httpx import structlog from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type  log = structlog.get_logger()  API_URL = os.environ.get("API_URL", "https://httpbin.org/json") TIMEOUT = httpx.Timeout(10.0, connect=5.0)  class FetchError(RuntimeError):     pass  @retry(     reraise=True,     stop=stop_after_attempt(4),     wait=wait_exponential(multiplier=0.5, min=0.5, max=5),     retry=retry_if_exception_type((httpx.HTTPError, FetchError)), ) def fetch_json(client: httpx.Client, url: str) -> dict:     r = client.get(url)     if r.status_code >= 500:         # серверная ошибка — пробуем повторить         raise FetchError(f"server_error={r.status_code}")     r.raise_for_status()     return r.json()  def main() -> int:     structlog.configure(processors=[structlog.processors.add_log_level, structlog.processors.JSONRenderer()])     with httpx.Client(timeout=TIMEOUT, headers={"User-Agent": "fetch-users/1.0"}) as client:         t0 = time.perf_counter()         data = fetch_json(client, API_URL)         dt = time.perf_counter() - t0         log.info("fetched", bytes=len(json.dumps(data).encode("utf-8")), seconds=round(dt, 3))         print(json.dumps(data, ensure_ascii=False))     return 0  if __name__ == "__main__":     try:         sys.exit(main())     except httpx.RequestError as e:         log.error("http_error", error=str(e))         sys.exit(2)     except Exception as e:         log.error("unexpected", error=str(e.__class__.__name__))         sys.exit(3)

Запуск:

uv run fetch_users.py # закрепляем версии рядом со скриптом uv lock --script fetch_users.py # экспорт для CI uv export --script fetch_users.py -o requirements.txt uvx pip-audit -r requirements.txt

uv задокументировал и lock для скриптов, и экспорт.

Инспекция дерева зависимостей и политика источников

Перед выкладкой в хук pre‑commit можно обозреть дерево:

uv tree --script fetch_users.py

Для частных индексов и зеркал используем конфигурационные файлы uv.toml или pyproject.toml для uv pip и не раздаем --allow-insecure-host. Конфиги поддерживаются на уровне проекта и пользователя; есть и системный уровень. Путь к кешу и параметры можно контролировать через команды и переменные окружения.


Итог

PEP 723 решает рутину: однофайловые скрипты могут быть самодостаточными, без вопросов о том, как установить зависимости. uv сделал это быстрым и операционно удобным: редактирование зависимостей, shebang, lock рядом со скриптом, экспорт для CI, контроль версии интерпретатора.

Как и в случае с однофайловыми скриптами по PEP 723 и uv, которые позволяют быстро проверить идею без лишних подготовительных шагов, у вас есть возможность так же познакомиться с курсом Python Developer. Professional — через бесплатные открытые уроки, доступные по ссылке.

Кроме того, вы можете пройти бесплатное вступительное тестирование, которое позволит оценить ваши знания и навыки.

А если хотите узнать больше о самом курсе и впечатлениях участников, загляните в секцию с отзывами.


ссылка на оригинал статьи https://habr.com/ru/articles/940674/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *