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] успешно создан!")
Давайте разберем, что делает этот код:
Создание путей: Мы используем
pathlib.Pathи оператор/для интуитивного построения путей к корневой папке проекта и вложенной папке-пакету.Path.cwd()возвращает текущую рабочую директорию.Проверка: Перед созданием чего-либо мы проверяем, не существует ли уже такая папка. Это хороший тон для подобных утилит. Если папка есть, мы выводим ошибку и завершаем программу.
Создание папок и файлов:
package_path.mkdir(parents=True)создает всю необходимую иерархию папок..touch()создает пустой файл (идеально для__init__.py)..write_text()создает файл и записывает в него указанный контент.
Условная логика:
Если флаг
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 'Нет'}") # ... здесь мы будем вызывать наш движок ...
Что мы здесь сделали:
Последовательно вызвали
questionary.select()иquestionary.confirm()для каждого из наших вопросов.Добавили параметр
default, который определяет предварительно выбранный вариант. Это ускоряет работу для пользователя, если он согласен со стандартными настройками.Сохранили ответы в переменные
license_choice,gitignore_choiceиgit_choice.Важный момент: мы добавили проверку
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 для зависимостей. Добавьте в диалог два новых вопроса-подтверждения для создания этих файлов.
Подсказка:
Вам нужно будет добавить два новых вызова
questionary.confirm(), по аналогии с.gitignoreиgit.В "движке" (
engine.py) добавьте два новыхifблока.Используйте
path.write_text()для создания файлов. ВREADME.mdможно сразу записать заголовок с именем проекта (# {name}), аrequirements.txtна старте может быть пустым.
Уровень 2: Поддержка шаблонов проекта
Задача: Сделайте наш инструмент гораздо мощнее, добавив поддержку разных шаблонов. Вместо одной жестко заданной структуры, позвольте пользователю выбирать, какой проект он хочет создать: "простой скрипт" или, например, "базовое приложение FastAPI".
Подсказка:
Создайте в корне папку
templates, а внутри нее — две подпапки:simpleиfastapi. В каждой из них разместите соответствующую структуру файлов.Добавьте в
creator.pyновый вопросquestionary.select("Выберите шаблон проекта:", choices=["simple", "fastapi"]).Измените логику в
engine.py. Вместо того чтобы создавать файлы по одному (.touch(),.write_text()), используйте модульshutil(например,shutil.copytree()), чтобы скопировать все содержимое выбранной папки-шаблона в новую директорию проекта.
Уровень 3 (со звездочкой): Интеграция с Poetry
Задача: Современный Python-проект сложно представить без менеджера зависимостей. Добавьте опцию для автоматической инициализации проекта с помощью Poetry.
Подсказка:
Добавьте новый вопрос
questionary.confirm("Использовать Poetry для управления зависимостями?").Если пользователь ответил "да", то в
engine.pyпосле создания основной структуры папок вам нужно выполнить внешнюю команду.Используйте
subprocess.run(), чтобы запустить командуpoetry init --no-interactionв директории нового проекта. Флаг--no-interaction(или-n) очень важен — он говорит Poetry не задавать свои интерактивные вопросы, а использовать значения по умолчанию, что идеально подходит для автоматизации.
Анонс новых статей, полезные материалы, а так же если в процессе решения возникнут сложности, обсудить их или задать вопрос по статье можно в моём Telegram-сообществе.
Уверен, у вас все получится. Вперед, к практике!
