Use Flask's Click CLI with the app factory pattern
Asked Answered
P

4

20

I define my Flask application using the app factory pattern. When using Flask-Script, I can pass the factory function to the Manager. I'd like to use Flask's built-in Click CLI instead. How do I use the factory with Click?

My current code uses Flask-Script. How do I do this with Click?

from flask import Flask
from flask_script import Manager, Shell

def create_app():
    app = Flask(__name__)
    ...
    return app

manager = Manager(create_app)

def make_shell_context():
    return dict(app=app, db=db, User=User, Role=Role)

manager.add_command('shell', Shell(make_context=make_shell_context))

if __name__ == '__main__':
    manager.run()
Prescription answered 12/3, 2017 at 23:15 Comment(0)
N
19

The flask command is a Click interface created with flask.cli.FlaskGroup. Create your own group and pass it the factory function. Use app.shell_context_processor to add objects to the shell.

from flask import Flask
from flask.cli import FlaskGroup
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

def create_app(script_info=None):
    app = Flask(__name__)
    db.init_app(app)
    ...

    @app.shell_context_processor
    def shell_context():
        return {'app': app, 'db': db}

    return app

cli = FlaskGroup(create_app=create_app)

@cli.command
def custom_command():
    pass

if __name__ == '__main__':
    cli()

Run your file instead of the flask command. You'll get the Click interface using your factory.

FLASK_DEBUG=1 python app.py run

Ideally, create an entry point and install your package in your env. Then you can call the script as a command. Create a setup.py file with at least the following.

project/
    app/
        __init__.py
    setup.py
from setuptools import setup, find_packages

setup(
    name='my_app',
    version='1.0.0',
    packages=find_packages(),
    entry_points={
        'console_scripts': [
            'app=app:cli',
        ],
    },
)
pip install -e /path/to/project
FLASK_DEBUG=1 app run

Using your own CLI is less robust than the built-in flask command. Because your cli object is defined with your other code, a module-level error will cause the reloader to fail because it can no longer import the object. The flask command is separate from your project, so it's not affected by errors in your module.

Nikaniki answered 13/3, 2017 at 14:56 Comment(0)
F
4

Newly updated for Flask >= 2.1. See my other answer for Flask < 2.1.

In order to pass arguments to our app, we store them in script_info. And in order to do that, we create a custom Click interface using flask.cli.FlaskGroup.

However, passing script_info directly to app factories is deprecated in Flask 2, so we use Click's get_current_context function to get the current context and then access script_info from that context.

manage.py

#!/usr/bin/env python

import click
import config

from click import get_current_context
from flask import Flask
from flask.cli import FlaskGroup, pass_script_info


def create_app(*args, **kwargs):
    app = Flask(__name__)
    ctx = get_current_context(silent=True)

    if ctx:
        script_info = ctx.obj
        config_mode = script_info.config_mode
    elif kwargs.get("config_mode"):
        # Production server, e.g., gunincorn 
        # We don't have access to the current context, so must
        # read kwargs instead.
        config_mode = kwargs["config_mode"]

    ...    
    return app


@click.group(cls=FlaskGroup, create_app=create_app)
@click.option('-m', '--config-mode', default="Development")
@pass_script_info
def manager(script_info, config_mode):
    script_info.config_mode = config_mode
    ...


if __name__ == "__main__":
    manager()

Now you can run the dev server and set your desired config_mode by using either -m or --config-mode. Note, until Flask 2.1 drops you have to install Flask@aa13521d42bfdb

pip install git+https://github.com/pallets/flask.git@aa13521d42bfdb
python manage.py -m Production run

Production servers like gunincorn don't have access to the current context, so we pass what we need via kwargs.

gunicorn app:create_app\(config_mode=\'Production\'\) -w 3 -k gevent
Fauces answered 23/2, 2022 at 15:16 Comment(0)
F
1

In order to pass arguments to your app factory, you need to make use of script_info like so...

manage.py

#!/usr/bin/env python

import click
import config

from flask import Flask
from flask.cli import FlaskGroup, pass_script_info


def create_app(script_info):
    app = Flask(__name__)

    if script_info.config_mode:
        obj = getattr(config, script_info.config_mode)
        flask_config.from_object(obj)

    ...    
    return app


@click.group(cls=FlaskGroup, create_app=create_app)
@click.option('-m', '--config-mode', default="Development")
@pass_script_info
def manager(script_info, config_mode):
    script_info.config_mode = config_mode


if __name__ == "__main__":
    manager()

config.py

class Config(object):
    TESTING = False

class Production(Config):
    DATABASE_URI = 'mysql://user@localhost/foo'

class Development(Config):
    DATABASE_URI = 'sqlite:///app.db'

class Testing(Config):
    TESTING = True
    DATABASE_URI = 'sqlite:///:memory:'

now in the command line you can do manage -m Production run (after either adding the entry_points to setup.py as @davidism mentioned, or running pip install manage.py).

Fauces answered 9/4, 2020 at 22:16 Comment(0)
I
0

I found another way to add click.Options to all Flask commands and pass the options to create_app. It's a similar approach to @reubano's answer.

from flask import Flask
from flask.cli import FlaskGroup, ScriptInfo
import click

def create_app(say_hi: bool = False, say_goodbye: bool = False):
    app = Flask(__name__)
    
    if say_hi:
        print("hi")
    if say_goodbye:
        print("goodbye")

    return app


def update_create_app(ctx: click.Context, param: click.Option, value):
    """updates Flask's ScriptInfo instance for click commands, updating the create_app callable with our extra
    cli options."""
    if value is None:
        return None

    info = ctx.ensure_object(ScriptInfo)
    params = info.data.get("create_app_args", {})
    info.data["create_app_args"] = {**params, param.human_readable_name: value}
    info.create_app = lambda: create_app(**info.data["create_app_args"])
    return value


_hi = click.Option(
    ["--say-hi"],
    is_flag=True,
    default=False,
    callback=update_create_app,
)
_bye = click.Option(
    ["--say-goodbye"],
    is_flag=True,
    default=False,
    callback=update_create_app,
)


@click.group(cls=FlaskGroup, params=(_hi, _bye))
def customized_flask(*args, **kwargs):
    ...
Iila answered 24/4, 2023 at 13:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.