Merge pull request #4642 from pallets/cli-nest
`FlaskGroup` can be nested
This commit is contained in:
commit
fe4003b3c9
5 changed files with 53 additions and 39 deletions
|
|
@ -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
|
||||||
-------------
|
-------------
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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 "> ",
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue