Merge pull request #4642 from pallets/cli-nest

`FlaskGroup` can be nested
This commit is contained in:
David Lord 2022-06-15 14:17:45 -07:00 committed by GitHub
commit fe4003b3c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 53 additions and 39 deletions

View file

@ -25,6 +25,9 @@ Unreleased
- Added the ``View.init_every_request`` class attribute. If a view - Added the ``View.init_every_request`` class attribute. If a view
subclass sets this to ``False``, the view will not create a new subclass sets this to ``False``, the view will not create a new
instance on every request. :issue:`2520`. 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 Version 2.1.3
------------- -------------

View file

@ -10,6 +10,7 @@ from itertools import chain
from threading import Lock from threading import Lock
from types import TracebackType from types import TracebackType
import click
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from werkzeug.datastructures import ImmutableDict from werkzeug.datastructures import ImmutableDict
from werkzeug.exceptions import Aborter from werkzeug.exceptions import Aborter
@ -23,6 +24,7 @@ from werkzeug.routing import MapAdapter
from werkzeug.routing import RequestRedirect from werkzeug.routing import RequestRedirect
from werkzeug.routing import RoutingException from werkzeug.routing import RoutingException
from werkzeug.routing import Rule from werkzeug.routing import Rule
from werkzeug.serving import is_running_from_reloader
from werkzeug.urls import url_quote from werkzeug.urls import url_quote
from werkzeug.utils import redirect as _wz_redirect from werkzeug.utils import redirect as _wz_redirect
from werkzeug.wrappers import Response as BaseResponse from werkzeug.wrappers import Response as BaseResponse
@ -908,12 +910,18 @@ class Flask(Scaffold):
The default port is now picked from the ``SERVER_NAME`` The default port is now picked from the ``SERVER_NAME``
variable. variable.
""" """
# Change this into a no-op if the server is invoked from the # Ignore this call so that it doesn't start another server if
# command line. Have a look at cli.py for more information. # the 'flask run' command is used.
if os.environ.get("FLASK_RUN_FROM_CLI") == "true": 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 return
if get_load_dotenv(load_dotenv): if get_load_dotenv(load_dotenv):

View file

@ -5,12 +5,14 @@ import platform
import re import re
import sys import sys
import traceback import traceback
import typing as t
from functools import update_wrapper from functools import update_wrapper
from operator import attrgetter from operator import attrgetter
from threading import Lock from threading import Lock
from threading import Thread from threading import Thread
import click import click
from werkzeug.serving import is_running_from_reloader
from werkzeug.utils import import_string from werkzeug.utils import import_string
from .globals import current_app from .globals import current_app
@ -273,7 +275,7 @@ class DispatchingApp:
self._bg_loading_exc = None self._bg_loading_exc = None
if use_eager_loading is 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: if use_eager_loading:
self._load_unlocked() self._load_unlocked()
@ -477,7 +479,13 @@ class FlaskGroup(AppGroup):
if add_version_option: if add_version_option:
params.append(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.create_app = create_app
self.load_dotenv = load_dotenv self.load_dotenv = load_dotenv
self.set_debug_flag = set_debug_flag self.set_debug_flag = set_debug_flag
@ -545,26 +553,22 @@ class FlaskGroup(AppGroup):
return sorted(rv) return sorted(rv)
def main(self, *args, **kwargs): def make_context(
# Set a global flag that indicates that we were invoked from the self,
# command line interface. This is detected by Flask.run to make the info_name: t.Optional[str],
# call into a no-op. This is necessary to avoid ugly errors when the args: t.List[str],
# script that is loaded here also attempts to start a server. parent: t.Optional[click.Context] = None,
os.environ["FLASK_RUN_FROM_CLI"] = "true" **extra: t.Any,
) -> click.Context:
if get_load_dotenv(self.load_dotenv): if get_load_dotenv(self.load_dotenv):
load_dotenv() load_dotenv()
obj = kwargs.get("obj") if "obj" not in extra and "obj" not in self.context_settings:
extra["obj"] = ScriptInfo(
if obj is None:
obj = ScriptInfo(
create_app=self.create_app, set_debug_flag=self.set_debug_flag create_app=self.create_app, set_debug_flag=self.set_debug_flag
) )
kwargs["obj"] = obj return super().make_context(info_name, args, parent=parent, **extra)
kwargs.setdefault("auto_envvar_prefix", "FLASK")
return super().main(*args, **kwargs)
def _path_is_ancestor(path, other): def _path_is_ancestor(path, other):
@ -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, """Show extra startup messages the first time the server is run,
ignoring the reloader. ignoring the reloader.
""" """
if os.environ.get("WERKZEUG_RUN_MAIN") == "true": if is_running_from_reloader():
return return
if app_import_path is not None: if app_import_path is not None:
@ -653,10 +657,10 @@ def show_server_banner(env, debug, app_import_path, eager_loading):
if env == "production": if env == "production":
click.secho( click.secho(
" WARNING: This is a development server. Do not use it in" " 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", fg="red",
) )
click.secho(" Use a production WSGI server instead.", dim=True)
if debug is not None: if debug is not None:
click.echo(f" * Debug mode: {'on' if debug else 'off'}") click.echo(f" * Debug mode: {'on' if debug else 'off'}")
@ -963,6 +967,7 @@ def routes_command(sort: str, all_methods: bool) -> None:
cli = FlaskGroup( cli = FlaskGroup(
name="flask",
help="""\ help="""\
A general utility script for Flask applications. A general utility script for Flask applications.
@ -978,7 +983,7 @@ debug mode.
""".format( """.format(
cmd="export" if os.name == "posix" else "set", cmd="export" if os.name == "posix" else "set",
prefix="$ " if os.name == "posix" else "> ", prefix="$ " if os.name == "posix" else "> ",
) ),
) )

View file

@ -1,6 +1,4 @@
import os
import typing as t import typing as t
from warnings import warn
from .app import Flask from .app import Flask
from .blueprints import Blueprint from .blueprints import Blueprint
@ -159,16 +157,3 @@ def explain_template_loading_attempts(app: Flask, template, attempts) -> None:
info.append(" See https://flask.palletsprojects.com/blueprints/#templates") info.append(" See https://flask.palletsprojects.com/blueprints/#templates")
app.logger.info("\n".join(info)) 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,
)

View file

@ -388,6 +388,19 @@ def test_flaskgroup_debug(runner, set_debug_flag):
assert result.output == f"{not set_debug_flag}\n" 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(): def test_no_command_echo_loading_error():
from flask.cli import cli from flask.cli import cli