Skip to main content

Advanced CLI Customization and Custom Scripts

The Flask CLI is built on top of Click, but it introduces specialized structures to handle the unique requirements of a web framework, such as application discovery, environment variable management, and automatic application contexts. The core of this implementation resides in src/flask/cli.py through two primary classes: ScriptInfo and FlaskGroup.

ScriptInfo: The State Carrier

ScriptInfo is a helper object that manages the lifecycle and discovery of a Flask application during CLI execution. While it is often used internally by FlaskGroup, it is the object that eventually becomes the Click context object (ctx.obj) for commands.

Application Loading Logic

The primary responsibility of ScriptInfo is the load_app() method. It attempts to find a Flask instance using the following priority:

  1. Factory Function: If a create_app callback was provided during initialization.
  2. Import Path: If app_import_path is set (e.g., via the --app option or FLASK_APP environment variable).
  3. Default Files: It searches for wsgi.py or app.py in the current directory.
# src/flask/cli.py

def load_app(self) -> Flask:
if self._loaded_app is not None:
return self._loaded_app

app: Flask | None = None
if self.create_app is not None:
app = self.create_app()
else:
# ... logic to locate app via app_import_path or default files ...

if app is None:
raise NoAppException(...)

if self.set_debug_flag:
app.debug = get_debug_flag()

self._loaded_app = app
return app

ScriptInfo also maintains a data dictionary, allowing custom commands to store and retrieve arbitrary metadata across the command hierarchy.

FlaskGroup: The Command Hub

FlaskGroup is a specialized subclass of AppGroup (which itself inherits from click.Group). It is designed to automatically inject standard Flask commands and handle application-aware command discovery.

When you use FlaskGroup, it automatically:

  • Adds the run, shell, and routes commands.
  • Adds global options like --app, --debug, and --env-file.
  • Loads commands defined on the Flask application instance itself (app.cli).
  • Loads plugin commands registered via the flask.commands entry point.

Automatic Context Management

One of the most powerful features of FlaskGroup is how it handles the Flask application context. In get_command, it ensures that an app context is pushed before the command is executed, provided the app can be loaded.

# src/flask/cli.py

def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
# ... (plugin loading) ...
rv = super().get_command(ctx, name)
if rv is not None:
return rv

info = ctx.ensure_object(ScriptInfo)
try:
app = info.load_app()
except NoAppException:
return None

if not current_app or current_app._get_current_object() is not app:
ctx.with_resource(app.app_context())

return app.cli.get_command(ctx, name)

This means that for commands registered under a FlaskGroup, you often do not need the @with_appcontext decorator; the context is already active when the command body runs.

Custom Executable Scripts

For advanced use cases, such as creating a standalone management script for your project, you can instantiate FlaskGroup directly. This is useful when you want to provide a custom entry point that doesn't rely on the flask command.

As seen in tests/test_cli.py, you can define a custom CLI that uses a factory function to ensure the app is always loaded correctly:

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

def create_my_app():
return Flask("my_custom_app")

@click.group(cls=FlaskGroup, create_app=create_my_app)
def cli():
"""Management script for My Custom App."""
pass

@cli.command()
def custom_task():
"""A task that needs the app context."""
click.echo(f"Running task for {current_app.name}")

if __name__ == "__main__":
cli()

The FLASK_RUN_FROM_CLI Guard

When FlaskGroup initializes the Click context, it sets the environment variable FLASK_RUN_FROM_CLI="true". This is a critical internal signal used to prevent app.run() from executing if it is called during the import of the application (for example, if app.run() is not protected by a if __name__ == "__main__": block).

Manual ScriptInfo Usage

In scenarios where you are not using FlaskGroup but still need to invoke Flask commands (such as in unit tests or complex integration scripts), you can manually create a ScriptInfo object and pass it as the Click obj.

This pattern is used in src/flask/testing.py and demonstrated in tests/test_cli.py:

# Example of manual ScriptInfo usage in a test
from flask import Flask
from flask.cli import ScriptInfo
import click

@click.command()
def my_cmd():
click.echo(current_app.name)

def test_manual_script_info(runner):
# Manually define how the app should be created
obj = ScriptInfo(create_app=lambda: Flask("manual_app"))

# Pass the ScriptInfo as the context object
result = runner.invoke(my_cmd, obj=obj)
assert "manual_app" in result.output

By manually providing ScriptInfo, you gain control over exactly how the application is instantiated and configured before the CLI logic takes over.