🧙 Садовник: уточнил шаги, добавил проверки и обоснования. Примите, если полезно.

#1
open+1121proposed by gardener · Aug 4, 2026 · based on v1
Proposed changes · v1 → suggestion
+75166
1¶ ## Зачем это
2
3 Первое, что ломается в чужом проекте, — это окружение. «У меня работает» не является аргументом, если у соседа не собирается. Прежде чем написать первую строку кода, мы делаем так, чтобы проект одинаково поднимался на любой машине.
4
5 **Что должно быть готово до начала:** рабочее место из предыдущего списка — VS Code, git, Docker и uv установлены, папка проекта создана и открыта в редакторе.
6
7 Документация, которая пригодится: **[доки 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/)**
81## Создание проекта
91. Проверить, что uv на месте
10 Выполняй во встроенном терминале VS Code, находясь в папке проекта. Если команда не найдена вернись к списку про рабочее место.
2+1. Проверить установку uv
3+ Выполняй команду во встроенном терминале VS Code, находясь в папке будущего проекта. Если команда не найдена, сначала установи uv и заново открой терминал.
114 $ uv --version
12 why: Дальше всё идёт через uv. Проверить его сейчас десять секунд, выяснять потом посреди непонятной ошибки полчаса.
13 - [ ] Команда напечатала версию
14 - [ ] Терминал открыт в папке проекта, а не где-то ещё
152. Создать проект с флагом --package
16 Точка в конце означает «в текущей папке».
17
18 **Флаг `--package` обязателен.** Без него uv создаст простой скриптовый проект без раздела `[build-system]`, и свои команды (шаги ниже) просто не заработают `uv run lint` ответит `Failed to spawn: lint / program not found`.
19
20 Посмотри, что появилось: `pyproject.toml`, `README.md`, `.python-version`, `.gitignore` и папка `src/` с пакетом.
5+ why: Все последующие операции выполняются через uv, поэтому проблему с установкой лучше обнаружить сразу.
6+ - [ ] Команда напечатала версию uv
7+ - [ ] Терминал открыт в нужной папке
8+ - [ ] После установки uv команда доступна в новом терминале
9+ Установка uv https://docs.astral.sh/uv/getting-started/installation/
10+2. Создать пакетный проект
11+ Точка в конце означает «в текущей папке». Флаг `--package` создаёт устанавливаемый проект с разделом `[build-system]`, необходимым для собственных команд. После запуска проверь `pyproject.toml`, `README.md`, `.python-version`, `.gitignore` и папку `src/` с пакетом.
2112 $ uv init --package .
22 why: uv умеет два вида проектов: просто набор скриптов и настоящий устанавливаемый пакет. Нам нужен второй: только у него есть точки входа, из которых получаются команды проекта.
13+ why: Точки входа из `[project.scripts]` работают после установки проекта как пакета, а не как простого набора скриптов.
2314 - [ ] В папке появился pyproject.toml
24 - [ ] В нём есть раздел [build-system] — это главный признак, что флаг сработал
25 - [ ] Появилась папка src с пакетом внутри
263. Закрепить версию Python
27 После команды открой `.python-version` — там одна строка с версией.
28
29 Заодно поправь в `pyproject.toml` строку `requires-python` — uv мог поставить туда более старую версию:
30
31 ```toml
32 requires-python = ">=3.12"
33 ```
15+ - [ ] В pyproject.toml есть раздел [build-system]
16+ - [ ] Появилась папка src с пакетом
17+ Создание проектов uv — https://docs.astral.sh/uv/concepts/projects/init/
18+3. Закрепить Python 3.12
19+ После команды открой `.python-version`: там должна быть строка `3.12`. В `pyproject.toml` установи `requires-python = ">=3.12"`; это минимальная поддерживаемая версия, тогда как `.python-version` задаёт версию по умолчанию для работы с проектом.
3420 $ uv python pin 3.12
35 why: Без закреплённой версии проект соберётся под тем Python, который оказался на машине. Поведение между версиями отличается, и отлаживать такое тяжело: у тебя работает, у другого нет, код один и тот же.
21+ why: Явная версия по умолчанию уменьшает расхождения между локальными окружениями и не даёт случайно запустить проект на слишком старом Python.
3622 - [ ] Файл .python-version содержит 3.12
37 - [ ] В pyproject.toml requires-python тоже >=3.12
23+ - [ ] В pyproject.toml указано requires-python = ">=3.12"
24+ - [ ] Команда uv run python --version показывает Python 3.12
25+ → Версии Python в uv — https://docs.astral.sh/uv/concepts/python-versions/
3826## Структура
394. Переименовать пакет в app
40 uv создал `src/<имя_проекта>/`. Нам нужен пакет `app/` в корне.
41
42 Перенеси папку и удали пустую `src`, а затем скажи сборщику, где искать код — добавь в `pyproject.toml` перед разделом `[build-system]`:
27+4. Перенести пакеты в корень
28+ Перенеси созданный пакет из `src/<имя_проекта>/` в корневую папку `app/`, затем удали пустую `src`. Сразу создай папку `scripts/` с пустым `__init__.py` и добавь перед `[build-system]` настройку сборщика:
4329
4430 ```toml
4531 [tool.hatch.build.targets.wheel]
4632 packages = ["app", "scripts"]
4733 ```
48
49 Папки `scripts` ещё нет — создадим её через несколько шагов, поэтому вписываем сразу обе.
50 why: Сборщик по умолчанию ищет пакет с именем проекта в `src/`. Как только ты переименовал папку, ему надо сказать об этом явно — иначе установка проекта упадёт с сообщением, что пакет не найден.
51 - [ ] Папка app лежит в корне проекта
52 - [ ] Папки src больше нет
53 - [ ] В pyproject.toml есть раздел [tool.hatch.build.targets.wheel]
34+ why: После переноса сборщику нужно явно указать новые пути пакетов, иначе установка проекта завершится ошибкой или не включит нужный код.
35+ - [ ] Папка app находится в корне проекта
36+ - [ ] Папка scripts содержит __init__.py
37+ - [ ] В настройках wheel перечислены app и scripts
38+ Настройка сборки Hatch https://hatch.pypa.io/latest/config/build/
5439## Зависимости
555. Добавить первые зависимости
56 После команды открой `pyproject.toml` в разделе `dependencies` появились две строки:
57
58 ```toml
59 dependencies = [
60 "fastapi>=0.115.0",
61 "uvicorn>=0.30.0",
62 ]
63 ```
64
65 Заодно появился `uv.lock` и папка `.venv`. Загляни в `uv.lock` — он намного больше двух строк. Пойми, почему.
40+5. Добавить рабочие зависимости
41+ Команда добавит FastAPI и Uvicorn в `dependencies`, создаст или обновит `uv.lock` и синхронизирует `.venv`. Не копируй версии из примеров: проверь ограничения, которые uv записал для актуальных установленных выпусков.
6642 $ uv add fastapi uvicorn
67 why: В pyproject ты пишешь, что нужно проекту. В uv.lock uv записывает точные версии **всего дерева** включая зависимости твоих зависимостей. Именно lock-файл делает сборку воспроизводимой.
68 - [ ] В pyproject.toml в dependencies появились fastapi и uvicorn
69 - [ ] Создан файл uv.lock
70 - [ ] Ты можешь объяснить, почему uv.lock гораздо больше списка в pyproject
716. Запустить код в окружении проекта
72 Первая победа: пакет установлен и импортируется.
73
74 Попробуй ради эксперимента ту же строку без `uv run` — просто `python -c "import fastapi"`. Скорее всего упадёт с `ModuleNotFoundError`, и это правильно.
43+ why: В `pyproject.toml` перечислены прямые требования проекта, а `uv.lock` фиксирует разрешённое дерево прямых и транзитивных зависимостей.
44+ - [ ] В dependencies появились fastapi и uvicorn
45+ - [ ] Созданы uv.lock и .venv
46+ - [ ] Команда uv lock --check завершается успешно
47+ Зависимости проектов uv https://docs.astral.sh/uv/concepts/projects/dependencies/
48+6. Проверить окружение проекта
49+ Команда должна импортировать FastAPI из окружения проекта и напечатать его версию. Для сравнения можно выполнить ту же проверку без `uv run`, но результат будет зависеть от Python, который первым найден в `PATH`.
7550 $ uv run python -c "import fastapi; print(fastapi.__version__)"
76 why: `uv run` запускает команду **внутри окружения проекта**. Голый `python` берёт тот интерпретатор, что первым попался в PATH, и про твои зависимости ничего не знает. Почувствовать эту разницу сейчас — значит не ловить её потом часами.
51+ why: `uv run` синхронизирует окружение проекта и запускает команду с его интерпретатором и зависимостями.
7752 - [ ] Команда напечатала версию fastapi
78 - [ ] Ты попробовал то же без uv run и видел разницу
797. Добавить инструмент разработчика
80 Флаг `--dev` кладёт зависимость в отдельный раздел, а не в основной:
81
82 ```toml
83 [dependency-groups]
84 dev = [
85 "ruff>=0.16.0",
86 ]
87 ```
88
89 Сравни с тем, куда попали fastapi и uvicorn. Доки: **[ruff](https://docs.astral.sh/ruff/)**
53+ - [ ] uv run python --version соответствует закреплённой версии
54+ - [ ] Импорт завершается без ModuleNotFoundError
55+ Запуск команд через uv https://docs.astral.sh/uv/concepts/projects/run/
56+7. Добавить Ruff для разработки
57+ Флаг `--dev` помещает Ruff в группу `[dependency-groups].dev`, а не в основные `dependencies`. Проверь актуальное ограничение версии, которое uv записал в `pyproject.toml`.
9058 $ uv add --dev ruff
91 why: На проде нужен только код, который работает. Линтер, тесты и форматтер туда ехать не должны: они раздувают образ и добавляют то, что можно атаковать. Разделение заводится сразу, потом разгребать дороже.
92 - [ ] ruff оказался в разделе dev, а не в dependencies
93 - [ ] Ты можешь объяснить, почему это разные списки
59+ why: Отдельная группа не смешивает инструменты проверки кода с библиотеками, необходимыми приложению во время работы.
60+ - [ ] ruff находится в группе dev
61+ - [ ] ruff отсутствует в основных dependencies
62+ - [ ] Команда uv run ruff --version печатает версию
63+ → Документация Ruff — https://docs.astral.sh/ruff/
9464## Команды проекта
958. Написать первую команду
96 Создай папку `scripts/` с пустым `__init__.py` и файлом `commands.py`:
65+8. Создать и объявить команду lint
66+ Создай `scripts/commands.py`:
9767
9868 ```python
99 """Команды проекта. Объявляются в pyproject.toml, раздел [project.scripts]."""
69+ """Команды проекта, объявленные в pyproject.toml."""
10070
10171 import subprocess
10272 import sys
10373
10474
10575 def lint() -> None:
10676 """Проверить код линтером."""
10777 result = subprocess.run(["ruff", "check", "."], check=False)
10878 sys.exit(result.returncode)
10979 ```
11080
111 Обрати внимание на `sys.exit(...)`: код возврата пробрасывается наружу.
112 why: Без `sys.exit` команда всегда завершается успешно, даже когда линтер нашёл ошибки. Потом это выстрелит в CI: сборка зелёная, проверки красные, и никто не замечает.
113 - [ ] Файл scripts/commands.py создан
114 - [ ] В папке scripts есть __init__.py
115 - [ ] Функция возвращает код через sys.exit
1169. Объявить команду в pyproject.toml
117 Добавь раздел (если uv уже создал его с примером — замени содержимое):
81+ Затем добавь или обнови раздел:
11882
11983 ```toml
12084 [project.scripts]
12185 lint = "scripts.commands:lint"
12286 ```
12387
124 Слева от знака равенства — имя команды, справа — путь к функции в формате `модуль:функция`.
125 why: Смысл не в экономии символов, а в общем словаре: команда одна и та же на любой машине, её не надо помнить и негде ошибиться. Заодно это документация: по списку команд видно, что с проектом вообще делают.
126 - [ ] В pyproject.toml есть раздел [project.scripts] с командой lint
127 - [ ] Путь указан через двоеточие: модуль:функция
12810. Запустить свою команду
129 При первом запуске uv доустановит сам проект в окружениеувидишь строку вида `+ имя-проекта==0.1.0 (from file:///...)`. Это нормально и именно так рождаются команды.
130
131 Ожидаемый результат`All checks passed!`
132
133 **Если пишет `Failed to spawn: lint`** — значит нет раздела `[build-system]` или сборщик не видит папку `scripts`. Вернись к шагам про `--package` и `[tool.hatch.build.targets.wheel]`.
88+ Слева указано имя команды, справа — путь в формате `модуль:функция`.
89+ why: Проброс кода возврата через `sys.exit` позволяет терминалу и CI отличить успешную проверку от найденных ошибок.
90+ - [ ] Создан файл scripts/commands.py
91+ - [ ] В [project.scripts] объявлена команда lint
92+ - [ ] Функция завершается кодом Ruff через sys.exit
93+ Точки входа Python-проектовhttps://packaging.python.org/en/latest/guides/writing-pyproject-toml/#creating-executable-scripts
94+9. Запустить команду lint
95+ При первом запуске uv может переустановить сам проект в окружение это нормально. Если появляется `Failed to spawn: lint`, проверь наличие `[build-system]`, запись в `[project.scripts]` и включение `scripts` в настройки wheel.
13496 $ uv run lint
135 why: Здесь сходится всё предыдущее: проект устанавливаемый, сборщик знает про пакеты, точка входа объявлена. Любое звено мимо и команды нет.
136 - [ ] Команда отработала и напечатала результат проверки
137 - [ ] В выводе было видно, как устанавливается сам проект
13811. Проверить, что ничего не затенено
139 Команда печатает путь к тому файлу, который реально импортируется. Должен быть `scripts/commands.py`.
140
141 Запомни эту команду — она пригодится каждый раз, когда «код точно правильный, а ведёт себя странно».
97+ why: Этот запуск одновременно проверяет сборку пакета, установку точки входа и доступность Ruff в окружении разработки.
98+ - [ ] Команда запускается под именем lint
99+ - [ ] Ruff печатает результат проверки
100+ - [ ] При ошибках команда возвращает ненулевой код
101+10. Проверить путь импортируемой команды
102+ Команда печатает файл, из которого Python действительно импортирует модуль. Ожидается путь к `scripts/commands.py`; рядом не должно быть одноимённых файла и папки, способных запутать импорт.
142103 $ uv run python -c "import importlib.util as u; print(u.find_spec('scripts.commands').origin)"
143 why: Если рядом окажутся файл `commands.py` и папка `commands/`, Python выберет папку, а файл не импортируется никогда. Он будет выглядеть рабочим кодом, но будет мёртв. Ничего не падает — просто вызывается не тот код, который ты читаешь.
104+ why: Проверка обнаруживает затенение модулей, при котором Python выполняет не тот код, который разработчик редактирует.
144105 - [ ] Напечатан путь к scripts/commands.py
145 - [ ] В папке scripts нет одновременно файла и папки с одинаковым именем
106+ - [ ] В scripts нет одноимённых файла и папки
107+ - [ ] Путь относится к текущему проекту
146108## Git
14712. Разобраться, что идёт в репозиторий
148 uv уже создал `.gitignore`. Проверь, что в нём есть `.venv`, и что там **нет** `uv.lock`.
149
150 Минимум:
151
152 ```gitignore
153 .venv/
154 __pycache__/
155 *.py[cod]
156 .env
157 ```
158
159 Идёт в git: `pyproject.toml`, `uv.lock`, `.python-version`, код.
160 Не идёт: `.venv`, `__pycache__`, `.env`.
161 why: `.venv` — это собранные пакеты под конкретную ОС: у соседа они не заработают, а репозиторий раздуют. `uv.lock` — наоборот, текстовое описание того, какие версии нужны. Без него теряется весь смысл воспроизводимости.
162 - [ ] .venv перечислен в .gitignore
163 - [ ] uv.lock В .gitignore не перечислен
164 - [ ] .env перечислен, даже если файла ещё нет
16513. Проверить состав будущего коммита
166 В списке должны быть `pyproject.toml`, `uv.lock`, `.python-version`, `.gitignore`, папки `app` и `scripts`.
167
168 Папки `.venv` в списке быть не должно. Если она там есть — ты успел добавить её в git до того, как появился `.gitignore`. Лечится: `git rm -r --cached .venv`
169 $ git status
170 why: Проверять состав коммита до коммита — привычка, которая однажды спасёт от утечки секрета. Файл, попавший в историю, удалить оттуда почти невозможно.
171 - [ ] .venv не попадает в список
172 - [ ] uv.lock и .python-version в списке есть
173¶ ## Как это в бою
174
175 В боевом проекте, на который мы ориентируемся, — 978 файлов `.py`, и всё окружение поднимается одной командой. В корне лежат ровно те же три файла, что ты только что сделал.
176
177 Команд там четырнадцать: `dev`, `serve`, `worker`, `test`, `lint`, `format`, `migrate`, `deploy` и другие. Новый человек не выясняет, чем запускать проект, — он смотрит список команд.
178
179 **И что там вышло неудачно.** Рядом лежат файл `scripts/commands.py` и пакет `scripts/commands/` — с одинаковым именем. Python выбирает пакет, поэтому файл не импортируется **никогда**. Мёртвый код, который выглядит рабочим и вводит в заблуждение при чтении.
180
181 Второе: команда `bootstrap` описана как «полная инициализация проекта», а вызывает функцию `activate` — то есть делает не то, что заявлено.
182
183 Мы такого не повторяем: имя пакета и имя модуля в одной папке не совпадают, а команда делает ровно то, что написано в её описании. Поэтому шаг с `find_spec` выше — не формальность.
184¶ ## Задание
185
186 Добавь вторую команду — `format`, которая запускает `ruff format .` и точно так же пробрасывает код возврата.
187
188 Обрати внимание: назвать функцию просто `format` нельзя — так уже называется встроенная функция Python, и ты её затенишь. Назови `format_code`, а имя команды оставь `format`:
189
190 ```toml
191 [project.scripts]
192 lint = "scripts.commands:lint"
193 format = "scripts.commands:format_code"
194 ```
195
196 **Критерий готовности:** `uv run lint` и `uv run format` работают и делают разное. Создай файл с кривыми отступами и убедись, что первая ругается, а вторая правит.
197
198
199
200
201¶ ## Готово
202
203 У тебя проект, который восстанавливается одной командой на любой машине, с закреплённой версией Python и со своими командами.
204
205 Кода пока нет — и это нормально. Сначала строится то, во что код будет ложиться.
206
207 **Что дальше:** структура проекта — пять слоёв внутри `app/` и правило, по которому они зависят друг от друга. После этого — настройки, точка входа и первый эндпоинт.
109+11. Проверить файлы для коммита
110+ Убедись, что `.gitignore` содержит `.venv/`, `__pycache__/`, `*.py[cod]` и `.env`, но не содержит `uv.lock`. В Git должны попасть `pyproject.toml`, `uv.lock`, `.python-version`, `.gitignore`, `app/` и `scripts/`; если `.venv` уже отслеживается, удали её только из индекса командой `git rm -r --cached .venv`.
111+ $ git status --short && git check-ignore .venv
112+ why: Lock-файл обеспечивает воспроизводимость, а локальное окружение и секреты не должны раздувать репозиторий или попадать в его историю.
113+ - [ ] git check-ignore подтверждает игнорирование .venv
114+ - [ ] uv.lock и .python-version видны среди новых файлов
115+ - [ ] В списке нет .env, __pycache__ и содержимого .venv
116+ → Документация gitignore — https://git-scm.com/docs/gitignore
Review