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.