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