What Is Behind Flask Run Command

Originally Posted on with tags: flask
Last Update on

Chapter 2 of Miguel Grinberg’s Flask Web Development describes how to run a basic Flask app. Suppose you have a basic Flask app like below (from the book).

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return '<h1>Hello World</h1>'

You can use the Python 3 built-in venv module to create a virtual environment like below.

$ python -m venv venv
$ source venv/bin/activate
$ pip install flask

Here is output of pip freeze command. The pip install command installs several Flask dependencies.

click==7.1.2
Flask==1.1.2
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
Werkzeug==1.0.1

Then you can setup two environment variables and type flask run to start the app. But what happens when you start the command flask run? I am trying to find out the answer and this article documents the effort.

$ export FLASK_APP=hello.py
$ export FLASK_ENV=development
$ flask run

When you run the command pip install flask, the pip system installs a flask python script file in the venv/bin/ directory. You can think of flask as a command and run as an argument to the command. The run part is actually a sub-command in click. You can run flask --help to find out other available sub-commands.

The flask script file in venv/bin/ directory only has a few lines as shown below. The script is calling the main() function in the flask.cli module.

import re
import sys

from flask.cli import main

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(main())

Let’s open the cli.py file in venv\lib\python3.8\site-packages\flask directory. Line 965 defines the main function. The as_module argument is False and the main function calls cli.main method with two arguments args=['run', ] and prog_name=None. The cli part of cli.main is an instance of FlaskGroup class defined on Line 945.

def main(as_module=False):
    # TODO omit sys.argv once ... is fixed
    cli.main(args=sys.argv[1:], 
        prog_name="python -m flask" if as_module else None)

Here things become complicated and I have not figured everything out.

The __init__ method of FlaskGroup class (L487) adds three sub-commands to click. The add_command method is defined on L1343 Group class of click/core.py file.

if add_default_commands:
    self.add_command(run_command)
    self.add_command(shell_command)
    self.add_command(routes_command)

The FlaskGroup class is defined on Line 462 and it has a main method defined on Line 567 (code shown below). The method makes changes to some settings and calls the main method in super class.

def main(self, *args, **kwargs):

    os.environ["FLASK_RUN_FROM_CLI"] = "true"

    if get_load_dotenv(self.load_dotenv):
        load_dotenv()

    obj = kwargs.get("obj")

    if obj is None:
        obj = ScriptInfo(
            create_app=self.create_app, set_debug_flag=self.set_debug_flag
        )

    kwargs["obj"] = obj
    kwargs.setdefault("auto_envvar_prefix", "FLASK")
    return super(FlaskGroup, self).main(*args, **kwargs)

The FlaskGroup is derived from AppGroup class which is defined on Line 431. The AppGroup in turn is derived from click.Group class defined in core.py file in click package. The complete class inheritance tree is shown below. The main method in super class mentioned above is defined all the way up in click.BaseCommand class.

click.BaseCommand          /core.py Line 631
  click.Command                /core.py Line 832
    click.MultiCommand             /core.py Line 1069
      click.Group                 click/core.py Line 1331
        AppGroup                      cli.py Line 431
          FlaskGroup                      cli.py Line 462

Let’s get back to the flask/cli.py file and take a look at AppGroup class. The AppGroup class overrides two methods (decorators) command and group. It changes the behavior of the standard command decorator so that it automatically wraps the functions in with_appcontext. You can see examples of how those two decorators are used on the click documentation page.

The FlaskGroup class overrides get_command and list_commands methods in addition to the main method. The click documentation has a section Custom Multi Commands and the example in this section also overrides those two methods.

The actual run command is defined on Line 828 and the function is run_command. The command run becomes part of flask built in commands loaded by default. The code which loads the built in commands is in the FlaskGroup.get_command.

The run_command function look like this. It calls the run_simple function in werkzeug module and starts the development server. The DispatchingApp class is also interesting, and it loads the app based on the environment variable settings. I will discuss it in a later article.

@click.command("run", short_help="Run a development server.")
@click.option("--host", "-h", default="127.0.0.1", ...")
...
@pass_script_info
def run_command(info, host, port, ...):
    """Run a local development server."""
    ......
    show_server_banner(get_env(), debug, info.app_import_path, 
                       eager_loading)
    app = DispatchingApp(info.load_app, use_eager_loading=eager_loading)

    from werkzeug.serving import run_simple
    run_simple(host, port, app, ......)

The shell and routes commands are defined on Line 864 and Line 899 of the cli.py file.

The Flask documentation has a page Command Line Interface which has very good info on Flask CLI.

The nice thing about Flask cli module is that it is easy to add cli commands. Here is an example from Chapter 7 of Miguel Grinberg’s book. You can run the code with flask test cli command.

@app.cli.command()
def test():
    import unittest
    tests = unittest.TestLoader().discover('tests')
    unittest.TextTestRunner(verbosity=2).run(tests)

The app.cli is defined in _PackageBoundObject class of helpers.py module (L962). The Flask class is a subclass of _PackageBoundObject.

# code in __init__ method of _PackageBoundObject class
from .cli import AppGroup
self.cli = AppGroup()