- Регистрация
 - 23 Август 2023
 
- Сообщения
 - 3 041
 
- Лучшие ответы
 - 0
 
- Реакции
 - 0
 
- Баллы
 - 51
 
Offline
		
		
	
	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: А вот и настоящая звезда нашей статьи. Это библиотека, которая позволяет создавать красивые, интерактивные диалоги прямо в консоли — текстовые вопросы, списки выбора, подтверждения "да/нет" — с минимальными усилиями.
Прежде чем писать код нашего генератора, давайте подготовим рабочее окружение и познакомимся поближе с нашими основными инструментами.
Установка
Нам понадобятся три библиотеки. 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?

? Инициализировать Git-репозиторий?

После ответов на вопросы начнется магия, и вы увидите вывод нашего движка:
⚙️ Создание проекта 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 на старте может быть пустым.
Задача: Сделайте наш инструмент гораздо мощнее, добавив поддержку разных шаблонов. Вместо одной жестко заданной структуры, позвольте пользователю выбирать, какой проект он хочет создать: "простой скрипт" или, например, "базовое приложение FastAPI".
Подсказка:
Создайте в корне папку templates, а внутри нее — две подпапки: simple и fastapi. В каждой из них разместите соответствующую структуру файлов.
Добавьте в creator.py новый вопрос questionary.select("Выберите шаблон проекта:", choices=["simple", "fastapi"]).
Измените логику в engine.py. Вместо того чтобы создавать файлы по одному (.touch(), .write_text()), используйте модуль shutil (например, shutil.copytree()), чтобы скопировать все содержимое выбранной папки-шаблона в новую директорию проекта.
Задача: Современный Python-проект сложно представить без менеджера зависимостей. Добавьте опцию для автоматической инициализации проекта с помощью Poetry.
Подсказка:
Добавьте новый вопрос questionary.confirm("Использовать Poetry для управления зависимостями?").
Если пользователь ответил "да", то в engine.py после создания основной структуры папок вам нужно выполнить внешнюю команду.
Используйте subprocess.run(), чтобы запустить команду poetry init --no-interaction в директории нового проекта. Флаг --no-interaction (или -n) очень важен — он говорит Poetry не задавать свои интерактивные вопросы, а использовать значения по умолчанию, что идеально подходит для автоматизации.
Анонс новых статей, полезные материалы, а так же если в процессе решения возникнут сложности, обсудить их или задать вопрос по статье можно в моём Telegram-сообществе.
Уверен, у вас все получится. Вперед, к практике!