Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 18 из 20 Средний 35 мин 120 XP

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
Честная пометка: это вывод терминала на компьютере, в песочнице сервер не запускается. Адрес тот же, что давал app.run(), но файл приложения можно не трогать - он даже может не содержать вызова run().

Строка 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!
Те же флаги, что у app.run(debug=True), только управляются из терминала: код приложения не содержит настроек запуска, а окружение их задаёт. Debugger PIN у тебя будет свой - он генерируется случайно.

Переменная окружения здесь не случайность, а принцип: настройки запуска живут в окружении, а не в коде. 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
Сервер для этого не нужен: таблица собирается из правил маршрутизации. Пригодится, когда url_for упрямится и надо быстро сверить имя эндпоинта.

Свои команды: @app.cli.command

Настоящий проект — это не только сервер. Базу надо наполнять демонстрационными данными (сид), изредка чистить от устаревшего, пересчитывать агрегаты. Писать для этого скрипты-однодневки жалко: они теряются и не документируются. Flask предлагает держать такие операции командами приложения: декоратор @app.cli.command("имя") превращает обычную функцию в подкоманду flask. Вот модуль магазина с одной такой командой:

app.py — маршруты и первая команда
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 товара.")
Серверный модуль целиком - он понадобится прогону из следующего блока, который запишет его в файл. Первая строка функции - докстринг: flask выводит его как описание команды в справке.

Разбираем анатомию. Аргумент декоратора — имя команды: в терминале её будут звать 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
Сид - операция повторяемая: таблица создаётся с IF NOT EXISTS, старые данные удаляются, поэтому команду можно запускать сколько угодно раз. Именно так готовят демо-базы перед деплоем.

Вызов команды из Python: test_cli_runner

Команда — это код, а код без проверки быстро гниёт. Для команд у приложения есть test_cli_runner(): он вызывает подкоманды так же, как это делал бы терминал, но внутри Python — без процессов и окружения. Знакомый приём: тот же принцип, что у test_client из урока про тестирование, только для командной строки. Запусти блок: он пишет app.py целиком и вызывает команду программно:

Сид через test_cli_runner (запустите)
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 товара.
invoke возвращает объект результата: exit_code - то, чем команда попрощалась с терминалом (0 - успех), output - всё, что команда напечатала. В pytest-тесте здесь стояли бы ассерты.

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 только предупреждает:

Аргументы и опции: greet и drop-db (запустите)
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
Аргумент передаётся очередным элементом args, опция - строкой "--yes". is_flag=True делает --yes переключателем: без него yes равен False, с ним - True.

Обрати внимание на вторую пару: команда вызвалась, напечатала предупреждение — и вернула код 0. Это не ошибка, а осознанное поведение «сухого прогона»: drop-db без подтверждения ничего не делает, но и не ломается. Разрушительные операции в проектах часто строят именно так: без явного подтверждения — репетиция, с ним — действие.

Почему вообще команды, а не скрипты рядом с проектом? Скрипт-однодневка живёт своей жизнью: у него своя копия настроек подключения, свои пути и своё представление о том, где лежит база. Команда приложения всего этого не изобретает — она выполняется в контексте твоего проекта, видит его конфиг и его расширения, попадает в справку flask и, как ты видел, проверяется тем же pytest, что и остальной код. Одна строка декоратора — и операция перестала быть тайным знанием в папке scripts.

Когда команды нужнее сервера

Параметризованный сид: --count
@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 --app app seed-notes --count 10, из теста - invoke(args=["seed-notes", "--count", "10"]). show_default=True показывает дефолт в справке команды.
ЗадачаИнструментГде выполняется
Запустить сервер для разработки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)
Проверь себя
0 / 6

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()?

Карточки терминов
Запомнено: 0 / 7
Практика

Напиши команду stats: она печатает количество товаров из app.config["PRODUCTS"] (список из трёх названий). Внутри команды возьми конфиг через current_app.config и выведи строку «В каталоге N товара.» через click.echo. Вызови команду программно через test_cli_runner и напечатай её вывод и код выхода.

practice.py
Вопросы и ответы по уроку

Как правильно запустить 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.

Проверьте знания по Flask

В челлендже — 20 задач по Flask, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.

Тест по Flask: 20 задач