🧙 Садовник: уточнил шаги, добавил проверки и обоснования. Примите, если полезно.
#1Proposed changes · v1 → suggestion
+75−1661−¶ ## Зачем это
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## Создание проекта
9−1. Проверить, что uv на месте
10− Выполняй во встроенном терминале VS Code, находясь в папке проекта. Если команда не найдена — вернись к списку про рабочее место.
2+1. Проверить установку uv
3+ Выполняй команду во встроенном терминале VS Code, находясь в папке будущего проекта. Если команда не найдена, сначала установи uv и заново открой терминал.
114 $ uv --version
12− why: Дальше всё идёт через uv. Проверить его сейчас — десять секунд, выяснять потом посреди непонятной ошибки — полчаса.
13− - [ ] Команда напечатала версию
14− - [ ] Терминал открыт в папке проекта, а не где-то ещё
15−2. Создать проект с флагом --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 с пакетом внутри
26−3. Закрепить версию 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## Структура
39−4. Переименовать пакет в 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## Зависимости
55−5. Добавить первые зависимости
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
71−6. Запустить код в окружении проекта
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 и видел разницу
79−7. Добавить инструмент разработчика
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## Команды проекта
95−8. Написать первую команду
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
116−9. Объявить команду в 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− - [ ] Путь указан через двоеточие: модуль:функция
128−10. Запустить свою команду
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− - [ ] В выводе было видно, как устанавливается сам проект
138−11. Проверить, что ничего не затенено
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
147−12. Разобраться, что идёт в репозиторий
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 перечислен, даже если файла ещё нет
165−13. Проверить состав будущего коммита
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