Проверить установку uvAdded
Выполняй команду во встроенном терминале VS Code, находясь в папке будущего проекта. Если команда не найдена, сначала установи uv и заново открой терминал.
uv --versionСоздать пакетный проектAdded
Точка в конце означает «в текущей папке». Флаг --package создаёт устанавливаемый проект с разделом [build-system], необходимым для собственных команд. После запуска проверь pyproject.toml, README.md, .python-version, .gitignore и папку src/ с пакетом.
uv init --package .Закрепить Python 3.12Added
После команды открой .python-version: там должна быть строка 3.12. В pyproject.toml установи requires-python = ">=3.12"; это минимальная поддерживаемая версия, тогда как .python-version задаёт версию по умолчанию для работы с проектом.
uv python pin 3.12Перенести пакеты в кореньAdded
Перенеси созданный пакет из src/<имя_проекта>/ в корневую папку app/, затем удали пустую src. Сразу создай папку scripts/ с пустым __init__.py и добавь перед [build-system] настройку сборщика:
1[tool.hatch.build.targets.wheel]
2packages = ["app", "scripts"]
Добавить рабочие зависимостиAdded
Команда добавит FastAPI и Uvicorn в dependencies, создаст или обновит uv.lock и синхронизирует .venv. Не копируй версии из примеров: проверь ограничения, которые uv записал для актуальных установленных выпусков.
uv add fastapi uvicornПроверить окружение проектаAdded
Команда должна импортировать FastAPI из окружения проекта и напечатать его версию. Для сравнения можно выполнить ту же проверку без uv run, но результат будет зависеть от Python, который первым найден в PATH.
uv run python -c "import fastapi; print(fastapi.__version__)"Добавить Ruff для разработкиAdded
Флаг --dev помещает Ruff в группу [dependency-groups].dev, а не в основные dependencies. Проверь актуальное ограничение версии, которое uv записал в pyproject.toml.
uv add --dev ruffСоздать и объявить команду lintAdded
Создай scripts/commands.py:
1"""Команды проекта, объявленные в pyproject.toml."""
2
3import subprocess
4import sys
5
6
7def lint() -> None:
8 """Проверить код линтером."""
9 result = subprocess.run(["ruff", "check", "."], check=False)
10 sys.exit(result.returncode)
Затем добавь или обнови раздел:
1[project.scripts]
2lint = "scripts.commands:lint"
Слева указано имя команды, справа — путь в формате модуль:функция.
Запустить команду lintAdded
При первом запуске uv может переустановить сам проект в окружение — это нормально. Если появляется Failed to spawn: lint, проверь наличие [build-system], запись в [project.scripts] и включение scripts в настройки wheel.
uv run lintПроверить путь импортируемой командыAdded
Команда печатает файл, из которого Python действительно импортирует модуль. Ожидается путь к scripts/commands.py; рядом не должно быть одноимённых файла и папки, способных запутать импорт.
uv run python -c "import importlib.util as u; print(u.find_spec('scripts.commands').origin)"Проверить файлы для коммитаAdded
Убедись, что .gitignore содержит .venv/, __pycache__/, *.py[cod] и .env, но не содержит uv.lock. В Git должны попасть pyproject.toml, uv.lock, .python-version, .gitignore, app/ и scripts/; если .venv уже отслеживается, удали её только из индекса командой git rm -r --cached .venv.
git status --short && git check-ignore .venv## Зачем этоRemoved
## Зачем это
Первое, что ломается в чужом проекте, — это окружение. «У меня работает» не является аргументом, если у соседа не собирается. Прежде чем написать первую строку кода, мы делаем так, чтобы проект одинаково поднимался на любой машине.
**Что должно быть готово до начала:** рабочее место из предыдущего списка — VS Code, git, Docker и uv установлены, папка проекта создана и открыта в редакторе.
Документация, которая пригодится: **[доки uv](https://docs.astral.sh/uv/)** · **[проекты в uv](https://docs.astral.sh/uv/concepts/projects/)** · **[зависимости](https://docs.astral.sh/uv/concepts/projects/dependencies/)** · **[спецификация pyproject.toml](https://packaging.python.org/en/latest/specifications/pyproject-toml/)**
Проверить, что uv на местеRemoved
Выполняй во встроенном терминале VS Code, находясь в папке проекта. Если команда не найдена — вернись к списку про рабочее место.
Создать проект с флагом --packageRemoved
Точка в конце означает «в текущей папке».
**Флаг `--package` обязателен.** Без него uv создаст простой скриптовый проект без раздела `[build-system]`, и свои команды (шаги ниже) просто не заработают — `uv run lint` ответит `Failed to spawn: lint / program not found`.
Посмотри, что появилось: `pyproject.toml`, `README.md`, `.python-version`, `.gitignore` и папка `src/` с пакетом.
Закрепить версию PythonRemoved
После команды открой `.python-version` — там одна строка с версией.
Заодно поправь в `pyproject.toml` строку `requires-python` — uv мог поставить туда более старую версию:
```toml
requires-python = ">=3.12"
```
Переименовать пакет в appRemoved
uv создал `src/<имя_проекта>/`. Нам нужен пакет `app/` в корне.
Перенеси папку и удали пустую `src`, а затем скажи сборщику, где искать код — добавь в `pyproject.toml` перед разделом `[build-system]`:
```toml
[tool.hatch.build.targets.wheel]
packages = ["app", "scripts"]
```
Папки `scripts` ещё нет — создадим её через несколько шагов, поэтому вписываем сразу обе.
Добавить первые зависимостиRemoved
После команды открой `pyproject.toml` — в разделе `dependencies` появились две строки:
```toml
dependencies = [
"fastapi>=0.115.0",
"uvicorn>=0.30.0",
]
```
Заодно появился `uv.lock` и папка `.venv`. Загляни в `uv.lock` — он намного больше двух строк. Пойми, почему.
Запустить код в окружении проектаRemoved
Первая победа: пакет установлен и импортируется.
Попробуй ради эксперимента ту же строку без `uv run` — просто `python -c "import fastapi"`. Скорее всего упадёт с `ModuleNotFoundError`, и это правильно.
Добавить инструмент разработчикаRemoved
Флаг `--dev` кладёт зависимость в отдельный раздел, а не в основной:
```toml
[dependency-groups]
dev = [
"ruff>=0.16.0",
]
```
Сравни с тем, куда попали fastapi и uvicorn. Доки: **[ruff](https://docs.astral.sh/ruff/)**
Написать первую командуRemoved
Создай папку `scripts/` с пустым `__init__.py` и файлом `commands.py`:
```python
"""Команды проекта. Объявляются в pyproject.toml, раздел [project.scripts]."""
import subprocess
import sys
def lint() -> None:
"""Проверить код линтером."""
result = subprocess.run(["ruff", "check", "."], check=False)
sys.exit(result.returncode)
```
Обрати внимание на `sys.exit(...)`: код возврата пробрасывается наружу.
Объявить команду в pyproject.tomlRemoved
Добавь раздел (если uv уже создал его с примером — замени содержимое):
```toml
[project.scripts]
lint = "scripts.commands:lint"
```
Слева от знака равенства — имя команды, справа — путь к функции в формате `модуль:функция`.
Запустить свою командуRemoved
При первом запуске uv доустановит сам проект в окружение — увидишь строку вида `+ имя-проекта==0.1.0 (from file:///...)`. Это нормально и именно так рождаются команды.
Ожидаемый результат — `All checks passed!`
**Если пишет `Failed to spawn: lint`** — значит нет раздела `[build-system]` или сборщик не видит папку `scripts`. Вернись к шагам про `--package` и `[tool.hatch.build.targets.wheel]`.
Проверить, что ничего не затененоRemoved
Команда печатает путь к тому файлу, который реально импортируется. Должен быть `scripts/commands.py`.
Запомни эту команду — она пригодится каждый раз, когда «код точно правильный, а ведёт себя странно».
Разобраться, что идёт в репозиторийRemoved
uv уже создал `.gitignore`. Проверь, что в нём есть `.venv`, и что там **нет** `uv.lock`.
Минимум:
```gitignore
.venv/
__pycache__/
*.py[cod]
.env
```
Идёт в git: `pyproject.toml`, `uv.lock`, `.python-version`, код.
Не идёт: `.venv`, `__pycache__`, `.env`.
Проверить состав будущего коммитаRemoved
В списке должны быть `pyproject.toml`, `uv.lock`, `.python-version`, `.gitignore`, папки `app` и `scripts`.
Папки `.venv` в списке быть не должно. Если она там есть — ты успел добавить её в git до того, как появился `.gitignore`. Лечится: `git rm -r --cached .venv`
## Как это в боюRemoved
## Как это в бою
В боевом проекте, на который мы ориентируемся, — 978 файлов `.py`, и всё окружение поднимается одной командой. В корне лежат ровно те же три файла, что ты только что сделал.
Команд там четырнадцать: `dev`, `serve`, `worker`, `test`, `lint`, `format`, `migrate`, `deploy` и другие. Новый человек не выясняет, чем запускать проект, — он смотрит список команд.
**И что там вышло неудачно.** Рядом лежат файл `scripts/commands.py` и пакет `scripts/commands/` — с одинаковым именем. Python выбирает пакет, поэтому файл не импортируется **никогда**. Мёртвый код, который выглядит рабочим и вводит в заблуждение при чтении.
Второе: команда `bootstrap` описана как «полная инициализация проекта», а вызывает функцию `activate` — то есть делает не то, что заявлено.
Мы такого не повторяем: имя пакета и имя модуля в одной папке не совпадают, а команда делает ровно то, что написано в её описании. Поэтому шаг с `find_spec` выше — не формальность.
## ЗаданиеRemoved
## Задание
Добавь вторую команду — `format`, которая запускает `ruff format .` и точно так же пробрасывает код возврата.
Обрати внимание: назвать функцию просто `format` нельзя — так уже называется встроенная функция Python, и ты её затенишь. Назови `format_code`, а имя команды оставь `format`:
```toml
[project.scripts]
lint = "scripts.commands:lint"
format = "scripts.commands:format_code"
```
**Критерий готовности:** `uv run lint` и `uv run format` работают и делают разное. Создай файл с кривыми отступами и убедись, что первая ругается, а вторая правит.
## ГотовоRemoved
## Готово
У тебя проект, который восстанавливается одной командой на любой машине, с закреплённой версией Python и со своими командами.
Кода пока нет — и это нормально. Сначала строится то, во что код будет ложиться.
**Что дальше:** структура проекта — пять слоёв внутри `app/` и правило, по которому они зависят друг от друга. После этого — настройки, точка входа и первый эндпоинт.