AI Создаем свой create-react-app на Python: интерактивный генератор проектов с Typer и Questionary

AI

Редактор
Регистрация
23 Август 2023
Сообщения
3 041
Лучшие ответы
0
Реакции
0
Баллы
51
Offline
#1

1. Введение: Боль ручного создания проектов


Вспомните, как вы начинаете новый проект на Python. Скорее всего, это до боли знакомый ритуал, выполняемый на автопилоте в терминале:

$ mkdir my_cool_project
$ cd my_cool_project
$ mkdir my_cool_project
$ touch my_cool_project/__init__.py
$ touch main.py
$ touch .gitignore
$ git init
...


Этот процесс не просто утомителен и рутинен. Он чреват ошибками. Легко сделать опечатку в названии, забыть __init__.py или создать неконсистентную структуру, которая будет отличаться от ваших предыдущих проектов. Каждый раз мы тратим драгоценные минуты на механическую работу вместо того, чтобы сразу погрузиться в решение интересной задачи.

Но что, если я скажу, что этот процесс можно и нужно автоматизировать? В мире фронтенда уже давно стали стандартом такие инструменты, как create-react-app или vue create. Они задают несколько вопросов и за секунды разворачивают полностью настроенное рабочее окружение. Почему бы нам не создать такой же удобный помощник для своих Python-проектов?

В этой статье мы именно этим и займемcя. Мы напишем собственный интерактивный генератор проектов, используя мощную и элегантную связку двух библиотек:


  • Typer: Уже знакомый нам надежный фундамент для создания любого CLI-приложения. Он возьмет на себя парсинг команд и аргументов.


  • Questionary: А вот и настоящая звезда нашей статьи. Это библиотека, которая позволяет создавать красивые, интерактивные диалоги прямо в консоли — текстовые вопросы, списки выбора, подтверждения "да/нет" — с минимальными усилиями.
2. Часть I: Знакомство с нашими инструментами


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

Установка


Нам понадобятся три библиотеки. Typer и его зависимость Rich для создания CLI-каркаса и красивого вывода, и, конечно же, Questionary для интерактивных диалогов.

Установим все одной командой:

pip install typer rich questionary

Typer: Краткое напоминание


Как мы помним из прошлой статьи, Typer — это фундамент, который позволяет нам превратить обычную функцию Python в мощную команду для терминала. Структура нашего будущего приложения будет выглядеть примерно так:

import typer

app = typer.Typer()

@app.command()
def new(name: str):
"""
Создает новый проект с именем NAME.
"""
print(f"Начинаем создание проекта {name}...")

if __name__ == "__main__":
app()


Здесь Typer автоматически создает команду new, которая требует один обязательный аргумент — name. Это прочно, надежно, но не очень гибко. Что если мы хотим задать пользователю не один, а пять вопросов, причем с вариантами ответов?

Questionary: Магия интерактивности


А вот и главный герой нашей статьи. Questionary — это библиотека, которая превращает скучный ввод аргументов в дружелюбный интерактивный диалог. Вместо того чтобы заставлять пользователя читать --help и запоминать флаги, мы можем просто спросить его.

Давайте посмотрим на несколько "вау-примеров". Создайте временный файл test_q.py и попробуйте запустить их.

1. Простой текстовый вопрос

import questionary
name = questionary.text("Как назовем проект?").ask()
print(f"Отлично, создаем проект с именем: {name}")


В терминале это будет выглядеть так:

? Как назовем проект? ›


2. Вопрос с подтверждением (да/нет)

import questionary
use_git = questionary.confirm("Инициализировать Git-репозиторий?").ask()
if use_git:
print("Хорошо, запускаю git init...")
else:
print("Окей, работаем без git.")


Результат в терминале:

? Инициализировать Git-репозиторий? (y/N) ›


3. Вопрос со списком выбора
Это самая эффектная возможность.

import questionary
license_type = questionary.select(
"Какую лицензию вы хотите использовать?",
choices=[
"MIT",
"GPLv3",
"Apache 2.0",
"Без лицензии"
]).ask()
print(f"Выбрана лицензия: {license_type}")


А в терминале мы получим полноценное интерактивное меню:

? Какую лицензию вы хотите использовать? (Use arrow keys)
❯ MIT
GPLv3
Apache 2.0
Без лицензии


Метод .ask() в конце каждой строки — это то, что выводит вопрос на экран, ждет ответа пользователя и возвращает результат.

Как видите, Questionary позволяет создавать невероятно удобные и профессионально выглядящие CLI-интерфейсы всего одной строкой кода на каждый вопрос.

Теперь у нас есть все необходимое: Typer для создания команды new, а Questionary — для наполнения ее интерактивным содержанием. Пора приступать к написанию "движка" нашего генератора.

3. Часть II: «Движок» нашего генератора


Прежде чем мы начнем задавать красивые вопросы пользователю, давайте напишем ядро нашего приложения — функцию, которая будет выполнять всю "грязную" работу по созданию папок и файлов. Такой подход, при котором логика отделена от интерфейса, является ключевым принципом хорошего проектирования. Наш "движок" не должен ничего знать о Questionary, он будет просто получать на вход параметры и выполнять свою задачу.

Для работы с файловой системой мы будем использовать современный и объектно-ориентированный способ — встроенную библиотеку pathlib. Она избавляет нас от головной боли с конкатенацией строк и слешами, делая код более читаемым и надежным.

Проектируем функцию-ядро


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

import subprocess
from pathlib import Path
from rich import print

def generate_project_structure(
name: str,
license_type: str,
use_git: bool,
use_gitignore: bool
):
"""
Создает файловую структуру проекта на основе переданных параметров.
"""
print(f"⚙️ Создание проекта [bold green]{name}[/bold green]...")

# 1. Создаем пути
root_path = Path.cwd() / name
package_path = root_path / name

# 2. Проверка и создание папок
if root_path.exists():
print(f"[bold red]Ошибка:[/bold red] Директория '{name}' уже существует.")
raise typer.Exit()

package_path.mkdir(parents=True)

# 3. Создание базовых файлов
(package_path / "__init__.py").touch()
(root_path / "main.py").write_text('if __name__ == "__main__":\n print("Hello, World!")\n')

# 4. Условная логика: добавляем опции
if use_gitignore:
GITIGNORE_CONTENT = """
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
"""
(root_path / ".gitignore").write_text(GITIGNORE_CONTENT.strip())

if license_type != "Без лицензии":
# В реальном проекте тексты лицензий лучше хранить в отдельных файлах
LICENSE_TEMPLATES = {
"MIT": "MIT License Text...",
"GPLv3": "GPLv3 License Text...",
"Apache 2.0": "Apache 2.0 License Text..."
}
(root_path / "LICENSE").write_text(LICENSE_TEMPLATES.get(license_type, ""))

if use_git:
try:
subprocess.run(["git", "init"], cwd=root_path, check=True, capture_output=True)
print("✅ Инициализирован Git-репозиторий.")
except (subprocess.CalledProcessError, FileNotFoundError):
print("[yellow]Предупреждение:[/yellow] Не удалось инициализировать Git. Убедитесь, что git установлен и доступен в PATH.")

print(f"✨ Проект [bold green]{name}[/bold green] успешно создан!")



Давайте разберем, что делает этот код:


  1. Создание путей: Мы используем pathlib.Path и оператор / для интуитивного построения путей к корневой папке проекта и вложенной папке-пакету. Path.cwd() возвращает текущую рабочую директорию.


  2. Проверка: Перед созданием чего-либо мы проверяем, не существует ли уже такая папка. Это хороший тон для подобных утилит. Если папка есть, мы выводим ошибку и завершаем программу.


  3. Создание папок и файлов:

    • package_path.mkdir(parents=True) создает всю необходимую иерархию папок.


    • .touch() создает пустой файл (идеально для __init__.py).


    • .write_text() создает файл и записывает в него указанный контент.

  4. Условная логика:

    • Если флаг use_gitignore равен True, мы создаем файл .gitignore и записываем в него стандартный шаблон для Python-проектов.


    • Если выбрана лицензия, мы создаем файл LICENSE с соответствующим текстом.


    • Если use_git равен True, мы используем встроенный модуль subprocess для выполнения внешней команды git init. Важно указать cwd=root_path, чтобы команда выполнилась именно в директории нашего нового проекта.

Теперь у нас есть мощное и гибкое ядро, полностью изолированное от интерфейса. Эту функцию уже можно импортировать и использовать в других скриптах или даже тестировать отдельно.

В следующей части мы наконец-то соберем наш красивый интерактивный "пульт управления", который будет задавать вопросы и вызывать этот движок с правильными параметрами.

Часть III: Собираем интерактивный интерфейс


Итак, у нас есть мощный "движок", готовый создавать проекты по команде. Теперь наша задача — построить для этого движка красивую и удобную "панель управления". Мы сделаем это с помощью Typer для основной структуры и Questionary для интерактивного диалога.

Наша цель — создать команду new, которая принимает один аргумент (имя проекта), а затем задает серию уточняющих вопросов.

Основа на Typer


Давайте создадим основной каркас нашего CLI-приложения в файле creator.py.

# creator.py
import typer
from rich import print

# Импортируем наш движок из предыдущей части.
# Предполагается, что он лежит в файле engine.py
# from engine import generate_project_structure

app = typer.Typer(help="CLI для быстрого создания Python-проектов.")

@app.command()
def new(name: str = typer.Argument(..., help="Название нового проекта.")):
"""
Создает новую структуру проекта с помощью интерактивного помощника.
"""
print(f"🚀 Запускаем интерактивный помощник для проекта [bold cyan]{name}[/bold cyan]...")
# Здесь будет магия Questionary!

if __name__ == "__main__":
app()


Это уже рабочая основа. Если вы запустите python creator.py new my-project --help, вы увидите красиво отформатированную справку.

Задаем вопросы с Questionary


Теперь самое интересное. Внутри функции new мы последовательно вызовем questionary, чтобы собрать все необходимые данные от пользователя.

# ... (начало файла creator.py) ...
import questionary

# ... (пропускаем импорты и создание app) ...

@app.command()
def new(name: str = typer.Argument(..., help="Название нового проекта.")):
"""
Создает новую структуру проекта с помощью интерактивного помощника.
"""
print(f"🚀 Запускаем интерактивный помощник для проекта [bold cyan]{name}[/bold cyan]...")

# Вопрос 1: Выбор лицензии
license_choice = questionary.select(
"Какую лицензию вы хотите использовать?",
choices=["MIT", "GPLv3", "Apache 2.0", "Без лицензии"],
default="MIT"
).ask()

# Если пользователь прервал ввод (нажал Ctrl+C), .ask() вернет None
if license_choice is None:
print("[bold red]Создание проекта отменено.[/bold red]")
raise typer.Exit()

# Вопрос 2: Использовать .gitignore?
gitignore_choice = questionary.confirm(
"Создать файл .gitignore?",
default=True
).ask()
if gitignore_choice is None:
raise typer.Exit() # Также проверяем на отмену

# Вопрос 3: Инициализировать Git?
git_choice = questionary.confirm(
"Инициализировать Git-репозиторий?",
default=True
).ask()
if git_choice is None:
raise typer.Exit()

# Для демонстрации выведем собранные ответы
print("\n[bold]Ваш выбор:[/bold]")
print(f"- Лицензия: {license_choice}")
print(f"- .gitignore: {'Да' if gitignore_choice else 'Нет'}")
print(f"- Git: {'Да' if git_choice else 'Нет'}")

# ... здесь мы будем вызывать наш движок ...


Что мы здесь сделали:


  1. Последовательно вызвали questionary.select() и questionary.confirm() для каждого из наших вопросов.


  2. Добавили параметр default, который определяет предварительно выбранный вариант. Это ускоряет работу для пользователя, если он согласен со стандартными настройками.


  3. Сохранили ответы в переменные license_choice, gitignore_choice и git_choice.


  4. Важный момент: мы добавили проверку if choice is None. Метод .ask() возвращает None, если пользователь прерывает выполнение скрипта (например, нажатием Ctrl+C). Наша проверка позволяет корректно обработать этот случай и чисто завершить программу.

Теперь у нас есть полностью рабочий интерактивный интерфейс. Он собирает всю необходимую информацию и хранит ее в переменных. Остался последний, самый приятный шаг — соединить этот интерфейс с нашим движком, чтобы на основе ответов пользователя создавалась реальная структура проекта.

5. Часть IV: Соединяем все вместе


Мы проделали всю подготовительную работу: у нас есть мощный "движок", который умеет создавать файловую структуру, и красивый интерактивный интерфейс, который собирает пожелания пользователя. Остался последний, самый приятный шаг — соединить их.

Наша задача — в главной функции new, после того как мы получили все ответы от пользователя, вызвать нашу функцию generate_project_structure, передав ей эти ответы в качестве аргументов.

Финальный аккорд


Давайте внесем финальное изменение в наш creator.py. Мы уберем демонстрационный вывод ответов и заменим его реальным вызовом нашего движка.

# ... (внутри функции new, после всех вопросов) ...

# Убираем этот блок:
# print("\n[bold]Ваш выбор:[/bold]")
# print(f"- Лицензия: {license_choice}")
# print(f"- .gitignore: {'Да' if gitignore_choice else 'Нет'}")
# print(f"- Git: {'Да' if git_choice else 'Нет'}")

# И заменяем его одним вызовом нашего движка:
generate_project_structure(
name=name,
license_type=license_choice,
use_git=git_choice,
use_gitignore=gitignore_choice
)


Магия происходит именно здесь: мы передаем имя проекта, полученное от Typer, и ответы, собранные с помощью Questionary, напрямую в нашу логическую функцию. Благодаря нашему разделению на интерфейс и движок, финальный шаг оказался невероятно простым и чистым.

Полный код проекта


Чтобы вы могли убедиться, что все собрано правильно, вот полный код для обоих файлов нашего проекта.

Показать полный код

Файл №1: engine.py (Наш движок)

import subprocess
from pathlib import Path
import typer # Импортируем typer для typer.Exit()
from rich import print

def generate_project_structure(
name: str,
license_type: str,
use_git: bool,
use_gitignore: bool
):
"""
Создает файловую структуру проекта на основе переданных параметров.
"""
print(f"⚙️ Создание проекта [bold green]{name}[/bold green]...")

root_path = Path.cwd() / name
package_path = root_path / name

if root_path.exists():
print(f"[bold red]Ошибка:[/bold red] Директория '{name}' уже существует.")
raise typer.Exit()

package_path.mkdir(parents=True)

(package_path / "__init__.py").touch()
(root_path / "main.py").write_text('if __name__ == "__main__":\n print("Hello, World!")\n')

if use_gitignore:
GITIGNORE_CONTENT = "# Byte-compiled ... (полный текст .gitignore)"
(root_path / ".gitignore").write_text(GITIGNORE_CONTENT.strip())

if license_type != "Без лицензии":
LICENSE_TEMPLATES = {
"MIT": "MIT License Text...",
"GPLv3": "GPLv3 License Text...",
"Apache 2.0": "Apache 2.0 License Text..."
}
(root_path / "LICENSE").write_text(LICENSE_TEMPLATES.get(license_type, ""))

if use_git:
try:
subprocess.run(["git", "init"], cwd=root_path, check=True, capture_output=True)
print("✅ Инициализирован Git-репозиторий.")
except (subprocess.CalledProcessError, FileNotFoundError):
print("[yellow]Предупреждение:[/yellow] Не удалось инициализировать Git.")

print(f"✨ Проект [bold green]{name}[/bold green] успешно создан!")


Файл №2: creator.py (Наш интерактивный CLI)

import typer
import questionary
from rich import print
from engine import generate_project_structure

app = typer.Typer(help="CLI для быстрого создания Python-проектов.")

@app.command()
def new(name: str = typer.Argument(..., help="Название нового проекта.")):
"""
Создает новую структуру проекта с помощью интерактивного помощника.
"""
print(f"🚀 Запускаем интерактивный помощник для проекта [bold cyan]{name}[/bold cyan]...")

license_choice = questionary.select(
"Какую лицензию вы хотите использовать?",
choices=["MIT", "GPLv3", "Apache 2.0", "Без лицензии"],
default="MIT"
).ask()

if license_choice is None:
print("[bold red]Создание проекта отменено.[/bold red]")
raise typer.Exit()

gitignore_choice = questionary.confirm("Создать файл .gitignore?", default=True).ask()
if gitignore_choice is None:
raise typer.Exit()

git_choice = questionary.confirm("Инициализировать Git-репозиторий?", default=True).ask()
if git_choice is None:
raise typer.Exit()

# Вызываем наш движок с собранными параметрами
generate_project_structure(
name=name,
license_type=license_choice,
use_git=git_choice,
use_gitignore=gitignore_choice
)

if __name__ == "__main__":
app()
Демонстрация


Теперь давайте запустим нашу утилиту из терминала и посмотрим на результат:

$ python creator.py new my_awesome_project


Вы увидите интерактивный диалог:

🚀 Запускаем интерактивный помощник для проекта my_awesome_project...
? Какую лицензию вы хотите использовать? MIT
? Создать файл .gitignore? Yes
? Инициализировать Git-репозиторий? Yes


После ответов на вопросы начнется магия, и вы увидите вывод нашего движка:

⚙️ Создание проекта my_awesome_project...
✅ Инициализирован Git-репозиторий.
✨ Проект my_awesome_project успешно создан!


А в вашей текущей директории появится новая папка my_awesome_project с идеально созданной структурой. Поздравляю, вы создали свой собственный, по-настоящему полезный инструмент разработчика

6. Заключение и "Домашнее задание"


Поздравляю! Мы не просто написали очередной скрипт, а создали полноценный инструмент разработчика, который экономит время, снижает количество рутинных ошибок и обеспечивает консистентность создаваемых проектов. Теперь у вас есть свой собственный create-react-app, но для мира Python.

Но лучший способ по-настоящему освоить новый инструмент — это продолжить его использовать и улучшать. Я подготовил три идеи разной сложности, которые помогут вам расширить функционал нашего генератора и еще глубже погрузиться в мир создания профессиональных CLI-утилит.

Уровень 1: Больше опциональных файлов

Задача: Сейчас наш генератор создает самый минимум файлов. Но почти каждому проекту нужен README.md для описания и requirements.txt для зависимостей. Добавьте в диалог два новых вопроса-подтверждения для создания этих файлов.

Подсказка:


  1. Вам нужно будет добавить два новых вызова questionary.confirm(), по аналогии с .gitignore и git.


  2. В "движке" (engine.py) добавьте два новых if блока.


  3. Используйте path.write_text() для создания файлов. В README.md можно сразу записать заголовок с именем проекта (# {name}), а requirements.txt на старте может быть пустым.
Уровень 2: Поддержка шаблонов проекта

Задача: Сделайте наш инструмент гораздо мощнее, добавив поддержку разных шаблонов. Вместо одной жестко заданной структуры, позвольте пользователю выбирать, какой проект он хочет создать: "простой скрипт" или, например, "базовое приложение FastAPI".

Подсказка:


  1. Создайте в корне папку templates, а внутри нее — две подпапки: simple и fastapi. В каждой из них разместите соответствующую структуру файлов.


  2. Добавьте в creator.py новый вопрос questionary.select("Выберите шаблон проекта:", choices=["simple", "fastapi"]).


  3. Измените логику в engine.py. Вместо того чтобы создавать файлы по одному (.touch(), .write_text()), используйте модуль shutil (например, shutil.copytree()), чтобы скопировать все содержимое выбранной папки-шаблона в новую директорию проекта.
Уровень 3 (со звездочкой): Интеграция с Poetry

Задача: Современный Python-проект сложно представить без менеджера зависимостей. Добавьте опцию для автоматической инициализации проекта с помощью Poetry.

Подсказка:


  1. Добавьте новый вопрос questionary.confirm("Использовать Poetry для управления зависимостями?").


  2. Если пользователь ответил "да", то в engine.py после создания основной структуры папок вам нужно выполнить внешнюю команду.


  3. Используйте subprocess.run(), чтобы запустить команду poetry init --no-interaction в директории нового проекта. Флаг --no-interaction (или -n) очень важен — он говорит Poetry не задавать свои интерактивные вопросы, а использовать значения по умолчанию, что идеально подходит для автоматизации.

Анонс новых статей, полезные материалы, а так же если в процессе решения возникнут сложности, обсудить их или задать вопрос по статье можно в моём Telegram-сообществе.

Уверен, у вас все получится. Вперед, к практике!
 
Сверху Снизу