Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ Unreleased
- Added the ``View.init_every_request`` class attribute. If a view
subclass sets this to ``False``, the view will not create a new
instance on every request. :issue:`2520`.
- A ``flask.cli.FlaskGroup`` Click group can be nested as a
sub-command in a custom CLI. :issue:`3263`


Version 2.1.3
-------------
Expand Down
16 changes: 12 additions & 4 deletions src/flask/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from threading import Lock
from types import TracebackType

import click
from werkzeug.datastructures import Headers
from werkzeug.datastructures import ImmutableDict
from werkzeug.exceptions import Aborter
Expand All @@ -23,6 +24,7 @@
from werkzeug.routing import RequestRedirect
from werkzeug.routing import RoutingException
from werkzeug.routing import Rule
from werkzeug.serving import is_running_from_reloader
from werkzeug.urls import url_quote
from werkzeug.utils import redirect as _wz_redirect
from werkzeug.wrappers import Response as BaseResponse
Expand Down Expand Up @@ -908,12 +910,18 @@ def run(
The default port is now picked from the ``SERVER_NAME``
variable.
"""
# Change this into a no-op if the server is invoked from the
# command line. Have a look at cli.py for more information.
# Ignore this call so that it doesn't start another server if
# the 'flask run' command is used.
if os.environ.get("FLASK_RUN_FROM_CLI") == "true":
from .debughelpers import explain_ignored_app_run
if not is_running_from_reloader():
click.secho(
" * Ignoring a call to 'app.run()', the server is"
" already being run with the 'flask run' command.\n"
" Only call 'app.run()' in an 'if __name__ =="
' "__main__"\' guard.',
fg="red",
)

explain_ignored_app_run()
return

if get_load_dotenv(load_dotenv):
Expand Down
45 changes: 25 additions & 20 deletions src/flask/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
import re
import sys
import traceback
import typing as t
from functools import update_wrapper
from operator import attrgetter
from threading import Lock
from threading import Thread

import click
from werkzeug.serving import is_running_from_reloader
from werkzeug.utils import import_string

from .globals import current_app
Expand Down Expand Up @@ -273,7 +275,7 @@ def __init__(self, loader, use_eager_loading=None):
self._bg_loading_exc = None

if use_eager_loading is None:
use_eager_loading = os.environ.get("WERKZEUG_RUN_MAIN") != "true"
use_eager_loading = not is_running_from_reloader()

if use_eager_loading:
self._load_unlocked()
Expand Down Expand Up @@ -477,7 +479,13 @@ def __init__(
if add_version_option:
params.append(version_option)

AppGroup.__init__(self, params=params, **extra)
if "context_settings" not in extra:
extra["context_settings"] = {}

extra["context_settings"].setdefault("auto_envvar_prefix", "FLASK")

super().__init__(params=params, **extra)

self.create_app = create_app
self.load_dotenv = load_dotenv
self.set_debug_flag = set_debug_flag
Expand Down Expand Up @@ -545,26 +553,22 @@ def list_commands(self, ctx):

return sorted(rv)

def main(self, *args, **kwargs):
# Set a global flag that indicates that we were invoked from the
# command line interface. This is detected by Flask.run to make the
# call into a no-op. This is necessary to avoid ugly errors when the
# script that is loaded here also attempts to start a server.
os.environ["FLASK_RUN_FROM_CLI"] = "true"

def make_context(
self,
info_name: t.Optional[str],
args: t.List[str],
parent: t.Optional[click.Context] = None,
**extra: t.Any,
) -> click.Context:
if get_load_dotenv(self.load_dotenv):
load_dotenv()

obj = kwargs.get("obj")

if obj is None:
obj = ScriptInfo(
if "obj" not in extra and "obj" not in self.context_settings:
extra["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().main(*args, **kwargs)
return super().make_context(info_name, args, parent=parent, **extra)


def _path_is_ancestor(path, other):
Expand Down Expand Up @@ -637,7 +641,7 @@ def show_server_banner(env, debug, app_import_path, eager_loading):
"""Show extra startup messages the first time the server is run,
ignoring the reloader.
"""
if os.environ.get("WERKZEUG_RUN_MAIN") == "true":
if is_running_from_reloader():
return

if app_import_path is not None:
Expand All @@ -653,10 +657,10 @@ def show_server_banner(env, debug, app_import_path, eager_loading):
if env == "production":
click.secho(
" WARNING: This is a development server. Do not use it in"
" a production deployment.",
" a production deployment.\n Use a production WSGI server"
" instead.",
fg="red",
)
click.secho(" Use a production WSGI server instead.", dim=True)

if debug is not None:
click.echo(f" * Debug mode: {'on' if debug else 'off'}")
Expand Down Expand Up @@ -963,6 +967,7 @@ def routes_command(sort: str, all_methods: bool) -> None:


cli = FlaskGroup(
name="flask",
help="""\
A general utility script for Flask applications.

Expand All @@ -978,7 +983,7 @@ def routes_command(sort: str, all_methods: bool) -> None:
""".format(
cmd="export" if os.name == "posix" else "set",
prefix="$ " if os.name == "posix" else "> ",
)
),
)


Expand Down
15 changes: 0 additions & 15 deletions src/flask/debughelpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import os
import typing as t
from warnings import warn

from .app import Flask
from .blueprints import Blueprint
Expand Down Expand Up @@ -159,16 +157,3 @@ def explain_template_loading_attempts(app: Flask, template, attempts) -> None:
info.append(" See https://flask.palletsprojects.com/blueprints/#templates")

app.logger.info("\n".join(info))


def explain_ignored_app_run() -> None:
if os.environ.get("WERKZEUG_RUN_MAIN") != "true":
warn(
Warning(
"Silently ignoring app.run() because the application is"
" run from the flask command line executable. Consider"
' putting app.run() behind an if __name__ == "__main__"'
" guard to silence this warning."
),
stacklevel=3,
)
13 changes: 13 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,19 @@ def test():
assert result.output == f"{not set_debug_flag}\n"


def test_flaskgroup_nested(app, runner):
cli = click.Group("cli")
flask_group = FlaskGroup(name="flask", create_app=lambda: app)
cli.add_command(flask_group)

@flask_group.command()
def show():
click.echo(current_app.name)

result = runner.invoke(cli, ["flask", "show"])
assert result.output == "flask_test\n"


def test_no_command_echo_loading_error():
from flask.cli import cli

Expand Down