CLI и команды Flask: @app.cli и flask run
Восемнадцатый урок расширения Flask: как проект запускают по-настоящему — flask run и FLASK_APP, свои команды @app.cli.command для сидов и обслуживания и вызов команд из тестов через test_cli_runner.
Редакция Питоники
Все девятнадцать уроков курса приложение запускалось одинаково: app.run() в конце файла. Для песочницы этого хватало, но рабочий способ другой — и он же открывает дверь к командам обслуживания: наполнить базу демонстрационными данными, пересчитать счётчики, вычистить просроченное. Всё это делается не из браузера, а из терминала — командами Flask. Сегодня разбираем, как запускают настоящие проекты и как писать собственные команды.
flask run: запуск через командную строку
У Flask есть собственная команда запуска. Она должна знать, где лежит приложение: modern-способ — флаг --app с именем модуля, классический — переменная окружения FLASK_APP. В терминале это выглядит так (команды терминала в браузерной песочнице не выполняются — они живут на твоей машине):
# современный способ: флаг --app с именем модуля
$ flask --app app run
# классический: переменная окружения
$ export FLASK_APP=app # Linux и macOS
$ set FLASK_APP=app # Windows (cmd)
$ flask run
* Serving Flask app 'app' * Debug mode: off WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:5000 Press CTRL+C to quit
Строка flask --app app run читается так: программа flask, подкоманда run, приложение — модуль app. Внутри модуля Flask ищет объект с именем app — тот самый, что ты создавал строкой app = Flask(__name__). Если не найдёт, напишет об этом честно, и это первая ошибка, которую стоит проверить при «не запускается»:
$ flask --app app run --port 8000 # другой порт
$ flask --app app run --debug # режим отладки
* Serving Flask app 'app' * Debug mode: on * Running on http://127.0.0.1:8000 Press CTRL+C to quit * Restarting with stat * Debugger is active!
Переменная окружения здесь не случайность, а принцип: настройки запуска живут в окружении, а не в коде. FLASK_APP, порт, секретный ключ из урока про конфигурацию — всё это работает одинаково: терминал задаёт, приложение читает через os.environ. Из этого вырастает и удобство, и безопасность: один и тот же код запускается на твоей машине и на сервере с разными значениями, и ничего не надо править перед деплоем.
У программы flask есть и другие встроенные подкоманды. Самая полезная для отладки — routes: она печатает карту маршрутов приложения, как это делал app.url_map в уроке про блюпринты, только без строки Python-кода:
$ flask --app app routes
Endpoint Methods Rule -------- ------- ----------------------- index GET / static GET /static/<path:filename> tasks GET /api/tasks
Свои команды: @app.cli.command
Настоящий проект — это не только сервер. Базу надо наполнять демонстрационными данными (сид), изредка чистить от устаревшего, пересчитывать агрегаты. Писать для этого скрипты-однодневки жалко: они теряются и не документируются. Flask предлагает держать такие операции командами приложения: декоратор @app.cli.command("имя") превращает обычную функцию в подкоманду flask. Вот модуль магазина с одной такой командой:
import sqlite3
import click
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def index():
return "Главная страница магазина"
@app.route("/api/tasks")
def tasks():
return jsonify([{"id": 1, "title": "Купить кофе"}])
@app.cli.command("seed-db")
def seed_db():
"""Наполняет базу магазина демонстрационными товарами."""
db = sqlite3.connect("shop.db")
db.execute(
"CREATE TABLE IF NOT EXISTS products"
" (id INTEGER PRIMARY KEY, name TEXT)"
)
db.execute("DELETE FROM products")
names = ["Кофемолка", "Турка", "Френч-пресс"]
db.executemany(
"INSERT INTO products (name) VALUES (?)", [(n,) for n in names]
)
db.commit()
db.close()
click.echo("База наполнена: 3 товара.")
Разбираем анатомию. Аргумент декоратора — имя команды: в терминале её будут звать seed-db, с дефисом, хотя функция называется seed_db. Докстринг становится описанием — flask --app app routes и справка по командам его показывают. Внутри — обычный Python: sqlite3, INSERT, commit. Вывод печатается не print(), а click.echo() — click это библиотека командных строк, на которой собран flask, и она умеет правильно работать с кодировками и цветовыми кодами терминала.
Какие операции в реальных проектах становятся командами? Список удивительно устойчив от проекта к проекту: сид — наполнение базы демонстрационными данными перед показом; миграции — приведение схемы к новой версии; отчистка — удаление просроченных сессий, временных файлов, неотправленных писем; пересчёт — агрегаты и счётчики, которые дешевле построить разом, чем считать на каждый запрос; и выгрузки — CSV-отчёты для коллег, которые удобно генерировать ночью по расписанию. Общее у всех: их вызывают не пользователи сайта, а сам разработчик или планировщик — значит, им место в CLI, а не в скрытых маршрутах.
В терминале команда вызывается по имени после указания приложения:
$ flask --app app seed-db
База наполнена: 3 товара.
# список всех команд приложения
$ flask --app app
Вызов команды из Python: test_cli_runner
Команда — это код, а код без проверки быстро гниёт. Для команд у приложения есть test_cli_runner(): он вызывает подкоманды так же, как это делал бы терминал, но внутри Python — без процессов и окружения. Знакомый приём: тот же принцип, что у test_client из урока про тестирование, только для командной строки. Запусти блок: он пишет app.py целиком и вызывает команду программно:
import sqlite3
import click
from flask import Flask
app = Flask(__name__)
@app.cli.command("seed-db")
def seed_db():
"""Наполняет базу магазина демонстрационными товарами."""
db = sqlite3.connect("shop.db")
db.execute("CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT)")
db.execute("DELETE FROM products")
names = ["Кофемолка", "Турка", "Френч-пресс"]
db.executemany("INSERT INTO products (name) VALUES (?)", [(n,) for n in names])
db.commit()
db.close()
click.echo("База наполнена: 3 товара.")
runner = app.test_cli_runner()
result = runner.invoke(args=["seed-db"])
print("Код выхода:", result.exit_code)
print(result.output, end="")
Код выхода: 0 База наполнена: 3 товара.
runner.invoke(args=["seed-db"]) — это «набери в терминале flask seed-db и верни мне результат». Код выхода 0 — команда отработала без ошибок; упади она с исключением, invoke вернул бы код 1 и traceback в output. В тестах это превращается в две проверки: assert result.exit_code == 0 и assert "наполнена" in result.output.
Аргументы и опции команды
Настоящие команды принимают параметры: имя пользователя, флаг подтверждения, количество записей. За это отвечает click: декораторы @click.argument для позиционных аргументов и @click.option для опций с флагами. Команда greet требует имя, а drop-db без флага --yes только предупреждает:
import click
from flask import Flask
app = Flask(__name__)
@app.cli.command("greet")
@click.argument("name")
def greet(name):
"""Приветствует пользователя по имени."""
click.echo("Привет, " + name + "!")
@app.cli.command("drop-db")
@click.option("--yes", is_flag=True, help="Не спрашивать подтверждение.")
def drop_db(yes):
"""Удаляет базу. Без флага --yes только предупреждает."""
if not yes:
click.echo("Укажите --yes, чтобы удалить базу по-настоящему.")
return
click.echo("База удалена.")
runner = app.test_cli_runner()
result = runner.invoke(args=["greet", "Анна"])
print(result.output, end="")
print("Код выхода:", result.exit_code)
result = runner.invoke(args=["drop-db"])
print(result.output, end="")
print("Код выхода:", result.exit_code)
result = runner.invoke(args=["drop-db", "--yes"])
print(result.output, end="")
print("Код выхода:", result.exit_code)
Привет, Анна! Код выхода: 0 Укажите --yes, чтобы удалить базу по-настоящему. Код выхода: 0 База удалена. Код выхода: 0
Обрати внимание на вторую пару: команда вызвалась, напечатала предупреждение — и вернула код 0. Это не ошибка, а осознанное поведение «сухого прогона»: drop-db без подтверждения ничего не делает, но и не ломается. Разрушительные операции в проектах часто строят именно так: без явного подтверждения — репетиция, с ним — действие.
Почему вообще команды, а не скрипты рядом с проектом? Скрипт-однодневка живёт своей жизнью: у него своя копия настроек подключения, свои пути и своё представление о том, где лежит база. Команда приложения всего этого не изобретает — она выполняется в контексте твоего проекта, видит его конфиг и его расширения, попадает в справку flask и, как ты видел, проверяется тем же pytest, что и остальной код. Одна строка декоратора — и операция перестала быть тайным знанием в папке scripts.
Когда команды нужнее сервера
@app.cli.command("seed-notes")
@click.option("--count", default=3, show_default=True, help="Сколько заметок создать.")
def seed_notes(count):
"""Наполняет базу тестовыми заметками."""
for i in range(1, count + 1):
click.echo("Создаём заметку " + str(i))
| Задача | Инструмент | Где выполняется |
|---|---|---|
| Запустить сервер для разработки | flask run / app.run() | терминал машины |
| Карта маршрутов | flask --app app routes | терминал, без сервера |
| Сид и обслуживание базы | @app.cli.command | терминал и тесты |
| Проверка команды автотестом | test_cli_runner().invoke() | внутри pytest |
Итоги
Теперь проект запускается по-взрослому: flask run через --app или FLASK_APP, карта маршрутов одной подкомандой, а рутина обслуживания — собственными командами на @app.cli.command с аргументами и опциями click. И всё это проверяется автотестами: test_cli_runner зовёт команды так же, как терминал, и возвращает код выхода с выводом.
В следующем уроке поговорим о тёмной стороне работы веб-приложений: на твой сайт будут пытаться влить опасный ввод и подделывать чужие запросы. Экранирование, CSRF-токены и секретный ключ — три замка, которые ставятся почти бесплатно. Карта курса — самоучитель Flask.
Сид-команда — это обычная функция с декоратором @app.cli.command: терминал вызывает её по имени, а тесты — через test_cli_runner.
Сначала предскажи ответ в голове — это главный навык программиста.
commands = {}
def cli_command(name):
def wrap(fn):
commands[name] = fn
return fn
return wrap
@cli_command("seed-db")
def seed_db():
return "База наполнена"
args = ["seed-db"]
print(commands[args[0]]())
def invoke(args, commands):
name = args[0]
if name not in commands:
return "нет такой команды", 2
return commands[name](args[1:]), 0
commands = {"greet": lambda rest: "Привет, " + rest[0] + "!"}
out, code = invoke(["greet", "Оля"], commands)
print(out, code)
def invoke(args, commands):
name = args[0]
if name not in commands:
return "нет такой команды", 2
return commands[name](args[1:]), 0
commands = {}
out, code = invoke(["drop-db", "--yes"], commands)
print(out, code)
1. Как flask run узнаёт, какой модуль запускать?
2. Что делает @app.cli.command("seed-db")?
3. Как вызвать команду seed-db из теста?
4. Что означает result.exit_code после runner.invoke?
5. Чем @click.argument отличается от @click.option?
6. Почему вывод в командах печатают click.echo(), а не print()?
Напиши команду stats: она печатает количество товаров из app.config["PRODUCTS"] (список из трёх названий). Внутри команды возьми конфиг через current_app.config и выведи строку «В каталоге N товара.» через click.echo. Вызови команду программно через test_cli_runner и напечатай её вывод и код выхода.
Как правильно запустить Flask-приложение?
Командой flask run, назвав приложение: flask --app app run — или заранее задав переменную окружения FLASK_APP=app. Flask найдёт в модуле объект app и поднимет сервер разработки на http://127.0.0.1:5000. Вызов app.run() внутри файла для рабочего запуска не нужен — он остаётся удобным вариантом для самых маленьких проектов.
Ошибка Could not locate a Flask application — что делать?
Flask не понял, какой модуль запускать. Проверьте три вещи: задан ли флаг --app или переменная FLASK_APP; совпадает ли имя модуля с реальным файлом; есть ли в модуле объект с именем app. Если объект называется иначе, укажите его явно: flask --app mymodule:myapp.
Как написать свою команду для flask, например сид базы?
Украсьте функцию декоратором @app.cli.command("seed-db") и печатайте вывод через click.echo. Внутри команды доступен current_app — приложение, из которого берётся конфиг и база. Параметры добавляются декораторами @click.argument (позиционные) и @click.option (опции и флаги). Вызывается команда из терминала: flask --app app seed-db.
Как протестировать flask cli команду автотестом?
Через app.test_cli_runner(): result = app.test_cli_runner().invoke(args=["seed-db"]). Раннер выполняет команду так же, как терминал, и возвращает объект с exit_code и output. В тесте проверяют assert result.exit_code == 0 и наличие ожидаемых строк в result.output — без подпроцессов и настройки окружения.
Понравился урок? Сошлитесь на него
«Сид-команда — это обычная функция с декоратором @app.cli.command: терминал вызывает её по имени, а тесты — через test_cli_runner.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
Flask · Урок 17
Фабрика приложений: паттерн create_app
Семнадцатый урок расширения Flask: один глобальный app протекает состоянием между тестами. Паттерн «фабрика приложений» собирает свежий экземпляр с блюпринтом и конфигом на каждый тест.
Flask · Урок 2
Первое веб-приложение на Flask: маршруты и режим отладки
Второй урок курса Flask: несколько страниц в одном приложении, динамические маршруты /post/42, конвертеры типов, своя страница 404 и debug=True, который экономит перезапуски.
Flask · Урок 15
Тестирование Flask: pytest и test_client
Пятнадцатый урок курса Flask — мост в мир автотестов: проверки API из предыдущих уроков становятся тест-функциями pytest с assert, а app.test_client() работает внутри теста без сервера и сети.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
BeautifulSoup / Scrapy · Урок 7
Сохранение результатов: CSV, JSON и базы данных
Список словарей собран — теперь доведём его до файла: CSV для таблиц, JSON для вложенных структур, sqlite3 для больших объёмов. Плюс две классические ловушки: пустые строки и кракозябры в Excel.
сохранение данных pythonпарсинг данных с сайта в excel
aiogram · Урок 2
Обработчики сообщений в aiogram: команды, текст и фильтры
Учим бота различать сообщения: Router, команды Command и CommandStart, аргументы через CommandObject и магический фильтр F — и главное правило, что порядок хендлеров решает всё.
aiogram командыaiogram filters Command
Flask · Урок 5
База данных в Flask: SQLite и Flask-SQLAlchemy
Пятый урок курса Flask: данные переживают перезапуск сервера. Подключаем SQLite, описываем модели Flask-SQLAlchemy, проходим CRUD и собираем гостевую книгу, которая помнит всех гостей.
flask и база данных sqlalchemyflask sqlalchemy модель
Проверьте знания по Flask
В челлендже — 20 задач по Flask, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.
Тест по Flask: 20 задач