aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/Flask/py3/flask/cli.py
diff options
context:
space:
mode:
authorrobot-piglet <robot-piglet@yandex-team.com>2025-01-16 19:09:30 +0300
committerrobot-piglet <robot-piglet@yandex-team.com>2025-01-16 19:38:51 +0300
commit7a23ad1fa2a5561a3575177d7240d8a1aa499718 (patch)
treeb4932bad31f595149e7a42e88cf729919995d735 /contrib/python/Flask/py3/flask/cli.py
parentfbb15f5ab8a61fc7c50500e2757af0b47174d825 (diff)
downloadydb-7a23ad1fa2a5561a3575177d7240d8a1aa499718.tar.gz
Intermediate changes
commit_hash:ae9e37c897fc6d514389f7089184df33bf781005
Diffstat (limited to 'contrib/python/Flask/py3/flask/cli.py')
-rw-r--r--contrib/python/Flask/py3/flask/cli.py418
1 files changed, 241 insertions, 177 deletions
diff --git a/contrib/python/Flask/py3/flask/cli.py b/contrib/python/Flask/py3/flask/cli.py
index 77c1e25a9c..37a15ff2d8 100644
--- a/contrib/python/Flask/py3/flask/cli.py
+++ b/contrib/python/Flask/py3/flask/cli.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import ast
import inspect
import os
@@ -5,19 +7,23 @@ import platform
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 click.core import ParameterSource
+from werkzeug import run_simple
+from werkzeug.serving import is_running_from_reloader
from werkzeug.utils import import_string
from .globals import current_app
from .helpers import get_debug_flag
-from .helpers import get_env
from .helpers import get_load_dotenv
+if t.TYPE_CHECKING:
+ from .app import Flask
+
class NoAppException(click.UsageError):
"""Raised if an application cannot be found or loaded."""
@@ -44,8 +50,8 @@ def find_best_app(module):
elif len(matches) > 1:
raise NoAppException(
"Detected multiple Flask applications in module"
- f" {module.__name__!r}. Use 'FLASK_APP={module.__name__}:name'"
- f" to specify the correct one."
+ f" '{module.__name__}'. Use '{module.__name__}:name'"
+ " to specify the correct one."
)
# Search for app factory functions.
@@ -63,15 +69,15 @@ def find_best_app(module):
raise
raise NoAppException(
- f"Detected factory {attr_name!r} in module {module.__name__!r},"
+ f"Detected factory '{attr_name}' in module '{module.__name__}',"
" but could not call it without arguments. Use"
- f" \"FLASK_APP='{module.__name__}:{attr_name}(args)'\""
+ f" '{module.__name__}:{attr_name}(args)'"
" to specify arguments."
) from e
raise NoAppException(
"Failed to find Flask application or factory in module"
- f" {module.__name__!r}. Use 'FLASK_APP={module.__name__}:name'"
+ f" '{module.__name__}'. Use '{module.__name__}:name'"
" to specify one."
)
@@ -208,8 +214,6 @@ def prepare_import(path):
def locate_app(module_name, app_name, raise_if_not_found=True):
- __traceback_hide__ = True # noqa: F841
-
try:
__import__(module_name)
except ImportError:
@@ -251,7 +255,7 @@ def get_version(ctx, param, value):
version_option = click.Option(
["--version"],
- help="Show the flask version",
+ help="Show the Flask version.",
expose_value=False,
callback=get_version,
is_flag=True,
@@ -259,74 +263,6 @@ version_option = click.Option(
)
-class DispatchingApp:
- """Special application that dispatches to a Flask application which
- is imported by name in a background thread. If an error happens
- it is recorded and shown as part of the WSGI handling which in case
- of the Werkzeug debugger means that it shows up in the browser.
- """
-
- def __init__(self, loader, use_eager_loading=None):
- self.loader = loader
- self._app = None
- self._lock = Lock()
- self._bg_loading_exc = None
-
- if use_eager_loading is None:
- use_eager_loading = os.environ.get("WERKZEUG_RUN_MAIN") != "true"
-
- if use_eager_loading:
- self._load_unlocked()
- else:
- self._load_in_background()
-
- def _load_in_background(self):
- # Store the Click context and push it in the loader thread so
- # script_info is still available.
- ctx = click.get_current_context(silent=True)
-
- def _load_app():
- __traceback_hide__ = True # noqa: F841
-
- with self._lock:
- if ctx is not None:
- click.globals.push_context(ctx)
-
- try:
- self._load_unlocked()
- except Exception as e:
- self._bg_loading_exc = e
-
- t = Thread(target=_load_app, args=())
- t.start()
-
- def _flush_bg_loading_exception(self):
- __traceback_hide__ = True # noqa: F841
- exc = self._bg_loading_exc
-
- if exc is not None:
- self._bg_loading_exc = None
- raise exc
-
- def _load_unlocked(self):
- __traceback_hide__ = True # noqa: F841
- self._app = rv = self.loader()
- self._bg_loading_exc = None
- return rv
-
- def __call__(self, environ, start_response):
- __traceback_hide__ = True # noqa: F841
- if self._app is not None:
- return self._app(environ, start_response)
- self._flush_bg_loading_exception()
- with self._lock:
- if self._app is not None:
- rv = self._app
- else:
- rv = self._load_unlocked()
- return rv(environ, start_response)
-
-
class ScriptInfo:
"""Helper object to deal with Flask applications. This is usually not
necessary to interface with as it's used internally in the dispatching
@@ -336,25 +272,28 @@ class ScriptInfo:
onwards as click object.
"""
- def __init__(self, app_import_path=None, create_app=None, set_debug_flag=True):
+ def __init__(
+ self,
+ app_import_path: str | None = None,
+ create_app: t.Callable[..., Flask] | None = None,
+ set_debug_flag: bool = True,
+ ) -> None:
#: Optionally the import path for the Flask application.
- self.app_import_path = app_import_path or os.environ.get("FLASK_APP")
+ self.app_import_path = app_import_path
#: Optionally a function that is passed the script info to create
#: the instance of the application.
self.create_app = create_app
#: A dictionary with arbitrary data that can be associated with
#: this script info.
- self.data = {}
+ self.data: t.Dict[t.Any, t.Any] = {}
self.set_debug_flag = set_debug_flag
- self._loaded_app = None
+ self._loaded_app: Flask | None = None
- def load_app(self):
+ def load_app(self) -> Flask:
"""Loads the Flask app (if not yet loaded) and returns it. Calling
this multiple times will just result in the already loaded app to
be returned.
"""
- __traceback_hide__ = True # noqa: F841
-
if self._loaded_app is not None:
return self._loaded_app
@@ -377,9 +316,10 @@ class ScriptInfo:
if not app:
raise NoAppException(
- "Could not locate a Flask application. You did not provide "
- 'the "FLASK_APP" environment variable, and a "wsgi.py" or '
- '"app.py" module was not found in the current directory.'
+ "Could not locate a Flask application. Use the"
+ " 'flask --app' option, 'FLASK_APP' environment"
+ " variable, or a 'wsgi.py' or 'app.py' file in the"
+ " current directory."
)
if self.set_debug_flag:
@@ -396,15 +336,25 @@ pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True)
def with_appcontext(f):
"""Wraps a callback so that it's guaranteed to be executed with the
- script's application context. If callbacks are registered directly
- to the ``app.cli`` object then they are wrapped with this function
- by default unless it's disabled.
+ script's application context.
+
+ Custom commands (and their options) registered under ``app.cli`` or
+ ``blueprint.cli`` will always have an app context available, this
+ decorator is not required in that case.
+
+ .. versionchanged:: 2.2
+ The app context is active for subcommands as well as the
+ decorated callback. The app context is always available to
+ ``app.cli`` command and parameter callbacks.
"""
@click.pass_context
def decorator(__ctx, *args, **kwargs):
- with __ctx.ensure_object(ScriptInfo).load_app().app_context():
- return __ctx.invoke(f, *args, **kwargs)
+ if not current_app:
+ app = __ctx.ensure_object(ScriptInfo).load_app()
+ __ctx.with_resource(app.app_context())
+
+ return __ctx.invoke(f, *args, **kwargs)
return update_wrapper(decorator, f)
@@ -440,6 +390,94 @@ class AppGroup(click.Group):
return click.Group.group(self, *args, **kwargs)
+def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None:
+ if value is None:
+ return None
+
+ info = ctx.ensure_object(ScriptInfo)
+ info.app_import_path = value
+ return value
+
+
+# This option is eager so the app will be available if --help is given.
+# --help is also eager, so --app must be before it in the param list.
+# no_args_is_help bypasses eager processing, so this option must be
+# processed manually in that case to ensure FLASK_APP gets picked up.
+_app_option = click.Option(
+ ["-A", "--app"],
+ metavar="IMPORT",
+ help=(
+ "The Flask application or factory function to load, in the form 'module:name'."
+ " Module can be a dotted import or file path. Name is not required if it is"
+ " 'app', 'application', 'create_app', or 'make_app', and can be 'name(args)' to"
+ " pass arguments."
+ ),
+ is_eager=True,
+ expose_value=False,
+ callback=_set_app,
+)
+
+
+def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None:
+ # If the flag isn't provided, it will default to False. Don't use
+ # that, let debug be set by env in that case.
+ source = ctx.get_parameter_source(param.name) # type: ignore[arg-type]
+
+ if source is not None and source in (
+ ParameterSource.DEFAULT,
+ ParameterSource.DEFAULT_MAP,
+ ):
+ return None
+
+ # Set with env var instead of ScriptInfo.load so that it can be
+ # accessed early during a factory function.
+ os.environ["FLASK_DEBUG"] = "1" if value else "0"
+ return value
+
+
+_debug_option = click.Option(
+ ["--debug/--no-debug"],
+ help="Set debug mode.",
+ expose_value=False,
+ callback=_set_debug,
+)
+
+
+def _env_file_callback(
+ ctx: click.Context, param: click.Option, value: str | None
+) -> str | None:
+ if value is None:
+ return None
+
+ import importlib
+
+ try:
+ importlib.import_module("dotenv")
+ except ImportError:
+ raise click.BadParameter(
+ "python-dotenv must be installed to load an env file.",
+ ctx=ctx,
+ param=param,
+ ) from None
+
+ # Don't check FLASK_SKIP_DOTENV, that only disables automatically
+ # loading .env and .flaskenv files.
+ load_dotenv(value)
+ return value
+
+
+# This option is eager so env vars are loaded as early as possible to be
+# used by other options.
+_env_file_option = click.Option(
+ ["-e", "--env-file"],
+ type=click.Path(exists=True, dir_okay=False),
+ help="Load environment variables from this file. python-dotenv must be installed.",
+ is_eager=True,
+ expose_value=False,
+ callback=_env_file_callback,
+)
+
+
class FlaskGroup(AppGroup):
"""Special subclass of the :class:`AppGroup` group that supports
loading more commands from the configured Flask app. Normally a
@@ -455,8 +493,14 @@ class FlaskGroup(AppGroup):
:param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv`
files to set environment variables. Will also change the working
directory to the directory containing the first file found.
- :param set_debug_flag: Set the app's debug flag based on the active
- environment
+ :param set_debug_flag: Set the app's debug flag.
+
+ .. versionchanged:: 2.2
+ Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options.
+
+ .. versionchanged:: 2.2
+ An app context is pushed when running ``app.cli`` commands, so
+ ``@with_appcontext`` is no longer required for those commands.
.. versionchanged:: 1.0
If installed, python-dotenv will be used to load environment variables
@@ -465,19 +509,30 @@ class FlaskGroup(AppGroup):
def __init__(
self,
- add_default_commands=True,
- create_app=None,
- add_version_option=True,
- load_dotenv=True,
- set_debug_flag=True,
- **extra,
- ):
+ add_default_commands: bool = True,
+ create_app: t.Callable[..., Flask] | None = None,
+ add_version_option: bool = True,
+ load_dotenv: bool = True,
+ set_debug_flag: bool = True,
+ **extra: t.Any,
+ ) -> None:
params = list(extra.pop("params", None) or ())
+ # Processing is done with option callbacks instead of a group
+ # callback. This allows users to make a custom group callback
+ # without losing the behavior. --env-file must come first so
+ # that it is eagerly evaluated before --app.
+ params.extend((_env_file_option, _app_option, _debug_option))
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
@@ -520,9 +575,18 @@ class FlaskGroup(AppGroup):
# Look up commands provided by the app, showing an error and
# continuing if the app couldn't be loaded.
try:
- return info.load_app().cli.get_command(ctx, name)
+ app = info.load_app()
except NoAppException as e:
click.secho(f"Error: {e.format_message()}\n", err=True, fg="red")
+ return None
+
+ # Push an app context for the loaded app unless it is already
+ # active somehow. This makes the context available to parameter
+ # and command callbacks without needing @with_appcontext.
+ 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)
def list_commands(self, ctx):
self._load_plugin_commands()
@@ -545,26 +609,39 @@ class FlaskGroup(AppGroup):
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.
+ def make_context(
+ self,
+ info_name: str | None,
+ args: list[str],
+ parent: click.Context | None = None,
+ **extra: t.Any,
+ ) -> click.Context:
+ # Set a flag to tell app.run to become a no-op. If app.run was
+ # not in a __name__ == __main__ guard, it would start the server
+ # when importing, blocking whatever command is being called.
os.environ["FLASK_RUN_FROM_CLI"] = "true"
+ # Attempt to load .env and .flask env files. The --env-file
+ # option can cause another file to be loaded.
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 parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
+ if not args and self.no_args_is_help:
+ # Attempt to load --env-file and --app early in case they
+ # were given as env vars. Otherwise no_args_is_help will not
+ # see commands from app.cli.
+ _env_file_option.handle_parse_result(ctx, {}, [])
+ _app_option.handle_parse_result(ctx, {}, [])
+
+ return super().parse_args(ctx, args)
def _path_is_ancestor(path, other):
@@ -574,7 +651,7 @@ def _path_is_ancestor(path, other):
return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other
-def load_dotenv(path=None):
+def load_dotenv(path: str | os.PathLike | None = None) -> bool:
"""Load "dotenv" files in order of precedence to set environment variables.
If an env var is already set it is not overwritten, so earlier files in the
@@ -587,13 +664,17 @@ def load_dotenv(path=None):
:param path: Load the file at this location instead of searching.
:return: ``True`` if a file was loaded.
- .. versionchanged:: 1.1.0
- Returns ``False`` when python-dotenv is not installed, or when
- the given path isn't a file.
+ .. versionchanged:: 2.0
+ The current directory is not changed to the location of the
+ loaded file.
.. versionchanged:: 2.0
When loading the env files, set the default encoding to UTF-8.
+ .. versionchanged:: 1.1.0
+ Returns ``False`` when python-dotenv is not installed, or when
+ the given path isn't a file.
+
.. versionadded:: 1.0
"""
try:
@@ -609,15 +690,15 @@ def load_dotenv(path=None):
return False
- # if the given path specifies the actual file then return True,
- # else False
+ # Always return after attempting to load a given path, don't load
+ # the default files.
if path is not None:
if os.path.isfile(path):
return dotenv.load_dotenv(path, encoding="utf-8")
return False
- new_dir = None
+ loaded = False
for name in (".env", ".flaskenv"):
path = dotenv.find_dotenv(name, usecwd=True)
@@ -625,38 +706,21 @@ def load_dotenv(path=None):
if not path:
continue
- if new_dir is None:
- new_dir = os.path.dirname(path)
-
dotenv.load_dotenv(path, encoding="utf-8")
+ loaded = True
- return new_dir is not None # at least one file was located and loaded
+ return loaded # True if at least one file was located and loaded.
-def show_server_banner(env, debug, app_import_path, eager_loading):
+def show_server_banner(debug, app_import_path):
"""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:
- message = f" * Serving Flask app {app_import_path!r}"
-
- if not eager_loading:
- message += " (lazy loading)"
-
- click.echo(message)
-
- click.echo(f" * Environment: {env}")
-
- if env == "production":
- click.secho(
- " WARNING: This is a development server. Do not use it in"
- " a production deployment.",
- fg="red",
- )
- click.secho(" Use a production WSGI server instead.", dim=True)
+ click.echo(f" * Serving Flask app '{app_import_path}'")
if debug is not None:
click.echo(f" * Debug mode: {'on' if debug else 'off'}")
@@ -786,12 +850,6 @@ class SeparatedPathType(click.Path):
"is active if debug is enabled.",
)
@click.option(
- "--eager-loading/--lazy-loading",
- default=None,
- help="Enable or disable eager loading. By default eager "
- "loading is enabled if the reloader is disabled.",
-)
-@click.option(
"--with-threads/--without-threads",
default=True,
help="Enable or disable multithreading.",
@@ -822,7 +880,6 @@ def run_command(
port,
reload,
debugger,
- eager_loading,
with_threads,
cert,
extra_files,
@@ -833,9 +890,26 @@ def run_command(
This server is for development purposes only. It does not provide
the stability, security, or performance of production WSGI servers.
- The reloader and debugger are enabled by default if
- FLASK_ENV=development or FLASK_DEBUG=1.
+ The reloader and debugger are enabled by default with the '--debug'
+ option.
"""
+ try:
+ app = info.load_app()
+ except Exception as e:
+ if is_running_from_reloader():
+ # When reloading, print out the error immediately, but raise
+ # it later so the debugger or server can handle it.
+ traceback.print_exc()
+ err = e
+
+ def app(environ, start_response):
+ raise err from None
+
+ else:
+ # When not reloading, raise the error immediately so the
+ # command fails.
+ raise e from None
+
debug = get_debug_flag()
if reload is None:
@@ -844,10 +918,7 @@ def run_command(
if debugger is None:
debugger = debug
- show_server_banner(get_env(), debug, info.app_import_path, eager_loading)
- app = DispatchingApp(info.load_app, use_eager_loading=eager_loading)
-
- from werkzeug.serving import run_simple
+ show_server_banner(debug, info.app_import_path)
run_simple(
host,
@@ -862,6 +933,9 @@ def run_command(
)
+run_command.params.insert(0, _debug_option)
+
+
@click.command("shell", short_help="Run a shell in the app context.")
@with_appcontext
def shell_command() -> None:
@@ -873,13 +947,11 @@ def shell_command() -> None:
without having to manually configure the application.
"""
import code
- from .globals import _app_ctx_stack
- app = _app_ctx_stack.top.app
banner = (
f"Python {sys.version} on {sys.platform}\n"
- f"App: {app.import_name} [{app.env}]\n"
- f"Instance: {app.instance_path}"
+ f"App: {current_app.import_name}\n"
+ f"Instance: {current_app.instance_path}"
)
ctx: dict = {}
@@ -890,7 +962,7 @@ def shell_command() -> None:
with open(startup) as f:
eval(compile(f.read(), startup, "exec"), ctx)
- ctx.update(app.make_shell_context())
+ ctx.update(current_app.make_shell_context())
# Site, customize, or startup script can set a hook to call when
# entering interactive mode. The default one sets up readline with
@@ -963,22 +1035,14 @@ def routes_command(sort: str, all_methods: bool) -> None:
cli = FlaskGroup(
+ name="flask",
help="""\
A general utility script for Flask applications.
-Provides commands from Flask, extensions, and the application. Loads the
-application defined in the FLASK_APP environment variable, or from a wsgi.py
-file. Setting the FLASK_ENV environment variable to 'development' will enable
-debug mode.
-
-\b
- {prefix}{cmd} FLASK_APP=hello.py
- {prefix}{cmd} FLASK_ENV=development
- {prefix}flask run
-""".format(
- cmd="export" if os.name == "posix" else "set",
- prefix="$ " if os.name == "posix" else "> ",
- )
+An application to load must be given with the '--app' option,
+'FLASK_APP' environment variable, or with a 'wsgi.py' or 'app.py' file
+in the current directory.
+""",
)