flask/src/flask/cli.py

1136 lines
36 KiB
Python
Raw Normal View History

from __future__ import annotations
import ast
2023-12-14 08:58:13 -08:00
import collections.abc as cabc
import importlib.metadata
import inspect
2014-04-28 13:26:23 +02:00
import os
2019-01-06 16:17:33 -08:00
import platform
import re
2014-04-28 13:26:23 +02:00
import sys
import traceback
2022-06-15 14:07:00 -07:00
import typing as t
from functools import update_wrapper
from operator import itemgetter
2023-12-14 08:58:13 -08:00
from types import ModuleType
2014-04-28 13:26:23 +02:00
import click
from click.core import ParameterSource
2022-08-01 15:11:21 -07:00
from werkzeug import run_simple
from werkzeug.serving import is_running_from_reloader
from werkzeug.utils import import_string
2014-04-28 13:26:23 +02:00
from .globals import current_app
from .helpers import get_debug_flag
from .helpers import get_load_dotenv
2017-07-14 22:37:53 -07:00
if t.TYPE_CHECKING:
2023-12-14 08:58:13 -08:00
import ssl
from _typeshed.wsgi import StartResponse
from _typeshed.wsgi import WSGIApplication
from _typeshed.wsgi import WSGIEnvironment
from .app import Flask
2014-04-28 13:26:23 +02:00
class NoAppException(click.UsageError):
"""Raised if an application cannot be found or loaded."""
2023-12-14 08:58:13 -08:00
def find_best_app(module: ModuleType) -> Flask:
2014-04-28 13:26:23 +02:00
"""Given a module instance this tries to find the best possible
application in the module or raises an exception.
"""
from . import Flask
# Search for the most common names first.
for attr_name in ("app", "application"):
2014-04-28 13:26:23 +02:00
app = getattr(module, attr_name, None)
if isinstance(app, Flask):
2014-04-28 13:26:23 +02:00
return app
# Otherwise find the only object that is a Flask instance.
2020-04-03 18:33:40 -07:00
matches = [v for v in module.__dict__.values() if isinstance(v, Flask)]
2014-04-28 13:26:23 +02:00
2014-05-12 02:16:13 +02:00
if len(matches) == 1:
2014-04-28 13:26:23 +02:00
return matches[0]
elif len(matches) > 1:
raise NoAppException(
2020-04-04 11:39:03 -07:00
"Detected multiple Flask applications in module"
f" '{module.__name__}'. Use '{module.__name__}:name'"
" to specify the correct one."
)
# Search for app factory functions.
for attr_name in ("create_app", "make_app"):
app_factory = getattr(module, attr_name, None)
if inspect.isfunction(app_factory):
try:
app = app_factory()
if isinstance(app, Flask):
return app
except TypeError as e:
if not _called_with_wrong_args(app_factory):
2017-11-25 00:05:57 +01:00
raise
raise NoAppException(
f"Detected factory '{attr_name}' in module '{module.__name__}',"
2020-04-04 11:39:03 -07:00
" but could not call it without arguments. Use"
f" '{module.__name__}:{attr_name}(args)'"
2020-04-04 11:39:03 -07:00
" to specify arguments."
) from e
raise NoAppException(
2020-04-04 11:39:03 -07:00
"Failed to find Flask application or factory in module"
f" '{module.__name__}'. Use '{module.__name__}:name'"
2020-04-04 11:39:03 -07:00
" to specify one."
)
2014-04-28 13:26:23 +02:00
2023-12-14 08:58:13 -08:00
def _called_with_wrong_args(f: t.Callable[..., Flask]) -> bool:
"""Check whether calling a function raised a ``TypeError`` because
the call failed or because something in the factory raised the
error.
:param f: The function that was called.
:return: ``True`` if the call failed.
"""
tb = sys.exc_info()[2]
try:
while tb is not None:
if tb.tb_frame.f_code is f.__code__:
# In the function, it was called successfully.
return False
tb = tb.tb_next
# Didn't reach the function.
return True
finally:
# Delete tb to break a circular reference.
2019-05-31 10:55:08 -04:00
# https://docs.python.org/2/library/sys.html#sys.exc_info
del tb
2023-12-14 08:58:13 -08:00
def find_app_by_string(module: ModuleType, app_name: str) -> Flask:
"""Check if the given string is a variable name or a function. Call
a function to get the app instance, or return the variable directly.
"""
from . import Flask
# Parse app_name as a single expression to determine if it's a valid
# attribute name or function call.
try:
expr = ast.parse(app_name.strip(), mode="eval").body
except SyntaxError:
raise NoAppException(
f"Failed to parse {app_name!r} as an attribute name or function call."
) from None
if isinstance(expr, ast.Name):
name = expr.id
args = []
kwargs = {}
elif isinstance(expr, ast.Call):
# Ensure the function name is an attribute name only.
if not isinstance(expr.func, ast.Name):
raise NoAppException(
f"Function reference must be a simple name: {app_name!r}."
)
name = expr.func.id
# Parse the positional and keyword arguments as literals.
try:
args = [ast.literal_eval(arg) for arg in expr.args]
2023-12-14 08:58:13 -08:00
kwargs = {
kw.arg: ast.literal_eval(kw.value)
for kw in expr.keywords
if kw.arg is not None
}
except ValueError:
# literal_eval gives cryptic error messages, show a generic
# message with the full expression instead.
raise NoAppException(
f"Failed to parse arguments as literal values: {app_name!r}."
) from None
else:
raise NoAppException(
f"Failed to parse {app_name!r} as an attribute name or function call."
)
try:
attr = getattr(module, name)
except AttributeError as e:
raise NoAppException(
f"Failed to find attribute {name!r} in {module.__name__!r}."
) from e
# If the attribute is a function, call it with any args and kwargs
# to get the real application.
if inspect.isfunction(attr):
try:
app = attr(*args, **kwargs)
except TypeError as e:
if not _called_with_wrong_args(attr):
2017-11-25 00:05:57 +01:00
raise
raise NoAppException(
f"The factory {app_name!r} in module"
2020-04-04 11:39:03 -07:00
f" {module.__name__!r} could not be called with the"
" specified arguments."
) from e
else:
app = attr
if isinstance(app, Flask):
return app
raise NoAppException(
2020-04-04 11:39:03 -07:00
"A valid Flask application was not obtained from"
f" '{module.__name__}:{app_name}'."
)
2023-12-14 08:58:13 -08:00
def prepare_import(path: str) -> str:
2014-04-28 13:54:13 +02:00
"""Given a filename this will try to calculate the python path, add it
to the search path and return the actual module name that is expected.
"""
path = os.path.realpath(path)
2014-04-28 13:26:23 +02:00
fname, ext = os.path.splitext(path)
if ext == ".py":
path = fname
if os.path.basename(path) == "__init__":
path = os.path.dirname(path)
module_name = []
# move up until outside package structure (no __init__.py)
while True:
path, name = os.path.split(path)
module_name.append(name)
if not os.path.exists(os.path.join(path, "__init__.py")):
2014-04-28 13:26:23 +02:00
break
if sys.path[0] != path:
sys.path.insert(0, path)
2017-06-16 06:59:37 -07:00
return ".".join(module_name[::-1])
2014-04-28 13:26:23 +02:00
2023-12-14 08:58:13 -08:00
@t.overload
def locate_app(
module_name: str, app_name: str | None, raise_if_not_found: t.Literal[True] = True
) -> Flask: ...
2023-12-14 08:58:13 -08:00
@t.overload
def locate_app(
module_name: str, app_name: str | None, raise_if_not_found: t.Literal[False] = ...
) -> Flask | None: ...
2023-12-14 08:58:13 -08:00
def locate_app(
module_name: str, app_name: str | None, raise_if_not_found: bool = True
) -> Flask | None:
try:
__import__(module_name)
2021-10-25 11:18:48 -07:00
except ImportError:
# Reraise the ImportError if it occurred within the imported module.
# Determine this by checking whether the trace has a depth > 1.
2023-12-14 08:58:13 -08:00
if sys.exc_info()[2].tb_next: # type: ignore[union-attr]
2017-06-16 06:59:37 -07:00
raise NoAppException(
2021-10-25 11:18:48 -07:00
f"While importing {module_name!r}, an ImportError was"
f" raised:\n\n{traceback.format_exc()}"
) from None
elif raise_if_not_found:
2021-10-25 11:18:48 -07:00
raise NoAppException(f"Could not import {module_name!r}.") from None
else:
2023-12-14 08:58:13 -08:00
return None
module = sys.modules[module_name]
2017-06-16 06:59:37 -07:00
if app_name is None:
return find_best_app(module)
2014-04-28 13:26:23 +02:00
else:
return find_app_by_string(module, app_name)
2016-05-26 20:07:52 +02:00
2023-12-14 08:58:13 -08:00
def get_version(ctx: click.Context, param: click.Parameter, value: t.Any) -> None:
if not value or ctx.resilient_parsing:
return
flask_version = importlib.metadata.version("flask")
werkzeug_version = importlib.metadata.version("werkzeug")
click.echo(
2020-04-04 11:39:03 -07:00
f"Python {platform.python_version()}\n"
f"Flask {flask_version}\n"
f"Werkzeug {werkzeug_version}",
color=ctx.color,
2019-01-06 16:17:33 -08:00
)
ctx.exit()
version_option = click.Option(
["--version"],
help="Show the Flask version.",
expose_value=False,
callback=get_version,
is_flag=True,
is_eager=True,
)
2020-04-04 09:43:06 -07:00
class ScriptInfo:
"""Helper object to deal with Flask applications. This is usually not
2014-04-28 13:26:23 +02:00
necessary to interface with as it's used internally in the dispatching
2016-05-26 20:45:50 +02:00
to click. In future versions of Flask this object will most likely play
a bigger role. Typically it's created automatically by the
:class:`FlaskGroup` but you can also manually create it and pass it
onwards as click object.
2024-11-07 11:27:51 -08:00
.. versionchanged:: 3.1
Added the ``load_dotenv_defaults`` parameter and attribute.
2014-04-28 13:26:23 +02:00
"""
def __init__(
self,
app_import_path: str | None = None,
create_app: t.Callable[..., Flask] | None = None,
set_debug_flag: bool = True,
2024-11-07 11:27:51 -08:00
load_dotenv_defaults: bool = True,
) -> None:
2016-05-26 20:45:50 +02:00
#: Optionally the import path for the Flask application.
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.
2023-04-20 08:30:45 -07:00
self.data: dict[t.Any, t.Any] = {}
self.set_debug_flag = set_debug_flag
2024-11-07 11:27:51 -08:00
self.load_dotenv_defaults = get_load_dotenv(load_dotenv_defaults)
"""Whether default ``.flaskenv`` and ``.env`` files should be loaded.
``ScriptInfo`` doesn't load anything, this is for reference when doing
the load elsewhere during processing.
.. versionadded:: 3.1
"""
self._loaded_app: Flask | None = None
2014-04-28 13:26:23 +02:00
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.
"""
2014-04-28 13:26:23 +02:00
if self._loaded_app is not None:
return self._loaded_app
2024-08-06 16:46:15 -06:00
app: Flask | None = None
if self.create_app is not None:
2024-08-06 16:46:15 -06:00
app = self.create_app()
else:
if self.app_import_path:
path, name = (
2023-08-08 02:10:49 -07:00
re.split(r":(?![\\/])", self.app_import_path, maxsplit=1) + [None]
)[:2]
import_name = prepare_import(path)
app = locate_app(import_name, name)
else:
for path in ("wsgi.py", "app.py"):
import_name = prepare_import(path)
app = locate_app(import_name, None, raise_if_not_found=False)
2017-06-16 06:59:37 -07:00
2023-12-14 08:58:13 -08:00
if app is not None:
break
2017-06-16 06:59:37 -07:00
2023-12-14 08:58:13 -08:00
if app is None:
raise NoAppException(
"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:
# Update the app's debug flag through the descriptor so that
# other values repopulate as well.
app.debug = get_debug_flag()
self._loaded_app = app
return app
2014-04-28 13:26:23 +02:00
pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True)
2014-04-28 13:26:23 +02:00
2023-12-14 08:58:13 -08:00
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
2014-04-28 13:26:23 +02:00
2023-12-14 08:58:13 -08:00
def with_appcontext(f: F) -> F:
"""Wraps a callback so that it's guaranteed to be executed with the
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.
2014-04-28 13:26:23 +02:00
"""
@click.pass_context
2023-12-14 08:58:13 -08:00
def decorator(ctx: click.Context, /, *args: t.Any, **kwargs: t.Any) -> t.Any:
if not current_app:
2023-12-14 08:58:13 -08:00
app = ctx.ensure_object(ScriptInfo).load_app()
ctx.with_resource(app.app_context())
2023-12-14 08:58:13 -08:00
return ctx.invoke(f, *args, **kwargs)
2023-12-14 08:58:13 -08:00
return update_wrapper(decorator, f) # type: ignore[return-value]
2014-04-28 13:26:23 +02:00
2014-08-28 08:56:58 +02:00
class AppGroup(click.Group):
"""This works similar to a regular click :class:`~click.Group` but it
changes the behavior of the :meth:`command` decorator so that it
automatically wraps the functions in :func:`with_appcontext`.
Not to be confused with :class:`FlaskGroup`.
"""
2023-12-14 08:58:13 -08:00
def command( # type: ignore[override]
self, *args: t.Any, **kwargs: t.Any
) -> t.Callable[[t.Callable[..., t.Any]], click.Command]:
2014-08-28 08:56:58 +02:00
"""This works exactly like the method of the same name on a regular
:class:`click.Group` but it wraps callbacks in :func:`with_appcontext`
unless it's disabled by passing ``with_appcontext=False``.
"""
wrap_for_ctx = kwargs.pop("with_appcontext", True)
2023-12-14 08:58:13 -08:00
def decorator(f: t.Callable[..., t.Any]) -> click.Command:
2014-08-28 08:56:58 +02:00
if wrap_for_ctx:
f = with_appcontext(f)
2023-12-14 08:58:13 -08:00
return super(AppGroup, self).command(*args, **kwargs)(f) # type: ignore[no-any-return]
2014-08-28 08:56:58 +02:00
return decorator
2023-12-14 08:58:13 -08:00
def group( # type: ignore[override]
self, *args: t.Any, **kwargs: t.Any
) -> t.Callable[[t.Callable[..., t.Any]], click.Group]:
2014-08-28 09:35:06 +02:00
"""This works exactly like the method of the same name on a regular
:class:`click.Group` but it defaults the group class to
:class:`AppGroup`.
"""
kwargs.setdefault("cls", AppGroup)
2023-12-14 08:58:13 -08:00
return super().group(*args, **kwargs) # type: ignore[no-any-return]
2014-08-28 09:35:06 +02:00
2014-08-28 08:56:58 +02:00
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"],
2022-08-01 15:11:21 -07:00
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:
try:
2024-11-07 11:27:51 -08:00
import dotenv # noqa: F401
except ImportError:
2024-11-07 11:27:51 -08:00
# Only show an error if a value was passed, otherwise we still want to
# call load_dotenv and show a message without exiting.
if value is not None:
raise click.BadParameter(
"python-dotenv must be installed to load an env file.",
ctx=ctx,
param=param,
) from None
# Load if a value was passed, or we want to load default files, or both.
if value is not None or ctx.obj.load_dotenv_defaults:
load_dotenv(value, load_defaults=ctx.obj.load_dotenv_defaults)
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),
2024-11-07 11:27:51 -08:00
help=(
"Load environment variables from this file, taking precedence over"
" those set by '.env' and '.flaskenv'. Variables set directly in the"
" environment take highest precedence. python-dotenv must be installed."
),
is_eager=True,
expose_value=False,
callback=_env_file_callback,
)
2014-08-28 08:56:58 +02:00
class FlaskGroup(AppGroup):
"""Special subclass of the :class:`AppGroup` group that supports
2014-08-28 08:56:58 +02:00
loading more commands from the configured Flask app. Normally a
developer does not have to interface with this class but there are
some very advanced use cases for which it makes sense to create an
instance of this. see :ref:`custom-scripts`.
:param add_default_commands: if this is True then the default run and
2019-06-30 14:37:40 +05:30
shell commands will be added.
:param add_version_option: adds the ``--version`` option.
2017-07-14 22:37:53 -07:00
:param create_app: an optional callback that is passed the script info and
returns the loaded app.
: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.
2022-08-01 15:11:21 -07:00
:param set_debug_flag: Set the app's debug flag.
2017-07-14 22:37:53 -07:00
2024-11-07 11:27:51 -08:00
.. versionchanged:: 3.1
``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files.
.. versionchanged:: 2.2
2022-08-01 15:11:21 -07:00
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.
2017-07-14 22:37:53 -07:00
.. versionchanged:: 1.0
If installed, python-dotenv will be used to load environment variables
from :file:`.env` and :file:`.flaskenv` files.
"""
def __init__(
self,
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:
2024-08-06 16:46:15 -06:00
params: list[click.Parameter] = 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.
2022-08-01 15:11:21 -07:00
params.extend((_env_file_option, _app_option, _debug_option))
if add_version_option:
params.append(version_option)
2022-06-15 14:07:00 -07:00
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
2017-07-14 22:37:53 -07:00
self.load_dotenv = load_dotenv
self.set_debug_flag = set_debug_flag
if add_default_commands:
self.add_command(run_command)
self.add_command(shell_command)
self.add_command(routes_command)
2016-05-26 21:29:01 +02:00
self._loaded_plugin_commands = False
2023-12-14 08:58:13 -08:00
def _load_plugin_commands(self) -> None:
2016-05-26 21:29:01 +02:00
if self._loaded_plugin_commands:
return
if sys.version_info >= (3, 10):
from importlib import metadata
else:
# Use a backport on Python < 3.10. We technically have
# importlib.metadata on 3.8+, but the API changed in 3.10,
# so use the backport for consistency.
2024-08-06 16:46:15 -06:00
import importlib_metadata as metadata # pyright: ignore
for ep in metadata.entry_points(group="flask.commands"):
2016-05-26 21:29:01 +02:00
self.add_command(ep.load(), ep.name)
2016-05-26 21:29:01 +02:00
self._loaded_plugin_commands = True
2023-12-14 08:58:13 -08:00
def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
2016-05-26 21:29:01 +02:00
self._load_plugin_commands()
# Look up built-in and plugin commands, which should be
# available even if the app fails to load.
rv = super().get_command(ctx, name)
2016-05-26 21:29:01 +02:00
if rv is not None:
return rv
info = ctx.ensure_object(ScriptInfo)
# Look up commands provided by the app, showing an error and
# continuing if the app couldn't be loaded.
2014-04-28 13:26:23 +02:00
try:
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.
2023-12-14 08:58:13 -08:00
if not current_app or current_app._get_current_object() is not app: # type: ignore[attr-defined]
ctx.with_resource(app.app_context())
return app.cli.get_command(ctx, name)
2014-04-28 13:26:23 +02:00
2023-12-14 08:58:13 -08:00
def list_commands(self, ctx: click.Context) -> list[str]:
2016-05-26 21:29:01 +02:00
self._load_plugin_commands()
# Start with the built-in and plugin commands.
rv = set(super().list_commands(ctx))
info = ctx.ensure_object(ScriptInfo)
# Add commands provided by the app, showing an error and
# continuing if the app couldn't be loaded.
2014-04-28 13:26:23 +02:00
try:
rv.update(info.load_app().cli.list_commands(ctx))
except NoAppException as e:
# When an app couldn't be loaded, show the error message
# without the traceback.
click.secho(f"Error: {e.format_message()}\n", err=True, fg="red")
except Exception:
# When any other errors occurred during loading, show the
# full traceback.
click.secho(f"{traceback.format_exc()}\n", err=True, fg="red")
2014-04-28 13:26:23 +02:00
return sorted(rv)
2022-06-15 14:07:00 -07:00
def make_context(
self,
info_name: str | None,
args: list[str],
parent: click.Context | None = None,
2022-06-15 14:07:00 -07:00
**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"
2022-06-15 14:07:00 -07:00
if "obj" not in extra and "obj" not in self.context_settings:
extra["obj"] = ScriptInfo(
2024-11-07 11:27:51 -08:00
create_app=self.create_app,
set_debug_flag=self.set_debug_flag,
load_dotenv_defaults=self.load_dotenv,
)
2017-07-14 22:37:53 -07:00
2022-06-15 14:07:00 -07:00
return super().make_context(info_name, args, parent=parent, **extra)
2017-07-14 22:37:53 -07:00
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
if (not args and self.no_args_is_help) or (
len(args) == 1 and args[0] in self.get_help_option_names(ctx)
):
# 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)
2017-07-14 22:37:53 -07:00
2023-12-14 08:58:13 -08:00
def _path_is_ancestor(path: str, other: str) -> bool:
2017-07-14 22:37:53 -07:00
"""Take ``other`` and remove the length of ``path`` from it. Then join it
to ``path``. If it is the original value, ``path`` is an ancestor of
``other``."""
return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other
2017-07-14 22:37:53 -07:00
2024-11-07 11:27:51 -08:00
def load_dotenv(
path: str | os.PathLike[str] | None = None, load_defaults: bool = True
) -> bool:
"""Load "dotenv" files to set environment variables. A given path takes
precedence over ``.env``, which takes precedence over ``.flaskenv``. After
loading and combining these files, values are only set if the key is not
already set in ``os.environ``.
2017-07-14 22:37:53 -07:00
This is a no-op if `python-dotenv`_ is not installed.
.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme
2024-11-07 11:27:51 -08:00
:param path: Load the file at this location.
:param load_defaults: Search for and load the default ``.flaskenv`` and
``.env`` files.
:return: ``True`` if at least one env var was loaded.
.. versionchanged:: 3.1
Added the ``load_defaults`` parameter. A given path takes precedence
over default files.
2017-07-14 22:37:53 -07:00
.. 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.
2017-07-14 22:37:53 -07:00
.. versionadded:: 1.0
"""
try:
import dotenv
except ImportError:
if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"):
2018-03-22 23:38:52 +09:00
click.secho(
2024-11-07 11:27:51 -08:00
" * Tip: There are .env files present. Install python-dotenv"
" to use them.",
fg="yellow",
2019-07-03 12:23:16 -07:00
err=True,
)
2017-07-14 22:37:53 -07:00
return False
2024-11-07 11:27:51 -08:00
data: dict[str, str | None] = {}
2024-11-07 11:27:51 -08:00
if load_defaults:
for default_name in (".flaskenv", ".env"):
if not (default_path := dotenv.find_dotenv(default_name, usecwd=True)):
continue
2017-07-14 22:37:53 -07:00
2024-11-07 11:27:51 -08:00
data |= dotenv.dotenv_values(default_path, encoding="utf-8")
2017-07-14 22:37:53 -07:00
2024-11-07 11:27:51 -08:00
if path is not None and os.path.isfile(path):
data |= dotenv.dotenv_values(path, encoding="utf-8")
2017-07-14 22:37:53 -07:00
2024-11-07 11:27:51 -08:00
for key, value in data.items():
if key in os.environ or value is None:
2017-07-14 22:37:53 -07:00
continue
2024-11-07 11:27:51 -08:00
os.environ[key] = value
2017-07-14 22:37:53 -07:00
2024-11-07 11:27:51 -08:00
return bool(data) # True if at least one env var was loaded.
2023-12-14 08:58:13 -08:00
def show_server_banner(debug: bool, app_import_path: str | None) -> None:
"""Show extra startup messages the first time the server is run,
ignoring the reloader.
"""
if is_running_from_reloader():
return
if app_import_path is not None:
click.echo(f" * Serving Flask app '{app_import_path}'")
if debug is not None:
2020-04-04 11:39:03 -07:00
click.echo(f" * Debug mode: {'on' if debug else 'off'}")
class CertParamType(click.ParamType):
"""Click option type for the ``--cert`` option. Allows either an
existing file, the string ``'adhoc'``, or an import for a
:class:`~ssl.SSLContext` object.
"""
name = "path"
2023-12-14 08:58:13 -08:00
def __init__(self) -> None:
self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True)
2023-12-14 08:58:13 -08:00
def convert(
self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None
) -> t.Any:
try:
import ssl
except ImportError:
2019-05-31 11:45:38 -04:00
raise click.BadParameter(
'Using "--cert" requires Python to be compiled with SSL support.',
ctx,
param,
) from None
2019-05-31 11:45:38 -04:00
try:
return self.path_type(value, param, ctx)
except click.BadParameter:
value = click.STRING(value, param, ctx).lower()
if value == "adhoc":
try:
2020-02-09 16:01:23 -08:00
import cryptography # noqa: F401
except ImportError:
raise click.BadParameter(
2020-02-09 16:01:23 -08:00
"Using ad-hoc certificates requires the cryptography library.",
ctx,
param,
) from None
return value
obj = import_string(value, silent=True)
2020-04-04 09:25:54 -07:00
if isinstance(obj, ssl.SSLContext):
return obj
raise
2023-12-14 08:58:13 -08:00
def _validate_key(ctx: click.Context, param: click.Parameter, value: t.Any) -> t.Any:
"""The ``--key`` option must be specified when ``--cert`` is a file.
Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed.
"""
cert = ctx.params.get("cert")
is_adhoc = cert == "adhoc"
try:
import ssl
except ImportError:
is_context = False
else:
is_context = isinstance(cert, ssl.SSLContext)
if value is not None:
if is_adhoc:
raise click.BadParameter(
'When "--cert" is "adhoc", "--key" is not used.', ctx, param
)
if is_context:
raise click.BadParameter(
'When "--cert" is an SSLContext object, "--key" is not used.',
ctx,
param,
)
if not cert:
raise click.BadParameter('"--cert" must also be specified.', ctx, param)
ctx.params["cert"] = cert, value
else:
if cert and not (is_adhoc or is_context):
raise click.BadParameter('Required when using "--cert".', ctx, param)
return value
class SeparatedPathType(click.Path):
"""Click option type that accepts a list of values separated by the
OS's path separator (``:``, ``;`` on Windows). Each value is
validated as a :class:`click.Path` type.
"""
2023-12-14 08:58:13 -08:00
def convert(
self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None
) -> t.Any:
items = self.split_envvar_value(value)
2024-01-26 10:08:55 +08:00
# can't call no-arg super() inside list comprehension until Python 3.12
super_convert = super().convert
return [super_convert(item, param, ctx) for item in items]
@click.command("run", short_help="Run a development server.")
@click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.")
@click.option("--port", "-p", default=5000, help="The port to bind to.")
@click.option(
2022-04-18 08:52:01 -04:00
"--cert",
type=CertParamType(),
help="Specify a certificate file to use HTTPS.",
is_eager=True,
)
@click.option(
"--key",
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
callback=_validate_key,
expose_value=False,
help="The key file to use when specifying a certificate.",
)
@click.option(
"--reload/--no-reload",
default=None,
help="Enable or disable the reloader. By default the reloader "
"is active if debug is enabled.",
)
@click.option(
"--debugger/--no-debugger",
default=None,
help="Enable or disable the debugger. By default the debugger "
"is active if debug is enabled.",
)
@click.option(
"--with-threads/--without-threads",
default=True,
help="Enable or disable multithreading.",
)
@click.option(
"--extra-files",
default=None,
type=SeparatedPathType(),
help=(
"Extra files that trigger a reload on change. Multiple paths"
2020-04-04 11:39:03 -07:00
f" are separated by {os.path.pathsep!r}."
),
)
@click.option(
"--exclude-patterns",
default=None,
type=SeparatedPathType(),
help=(
"Files matching these fnmatch patterns will not trigger a reload"
" on change. Multiple patterns are separated by"
f" {os.path.pathsep!r}."
),
)
2014-04-28 13:26:23 +02:00
@pass_script_info
def run_command(
2023-12-14 08:58:13 -08:00
info: ScriptInfo,
host: str,
port: int,
reload: bool,
debugger: bool,
with_threads: bool,
cert: ssl.SSLContext | tuple[str, str | None] | t.Literal["adhoc"] | None,
extra_files: list[str] | None,
exclude_patterns: list[str] | None,
) -> None:
"""Run a local development server.
This server is for development purposes only. It does not provide
the stability, security, or performance of production WSGI servers.
2022-08-01 15:11:21 -07:00
The reloader and debugger are enabled by default with the '--debug'
option.
"""
try:
2024-08-06 16:46:15 -06:00
app: WSGIApplication = info.load_app() # pyright: ignore
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
2023-12-14 08:58:13 -08:00
def app(
environ: WSGIEnvironment, start_response: StartResponse
) -> cabc.Iterable[bytes]:
raise err from None
else:
# When not reloading, raise the error immediately so the
# command fails.
raise e from None
2022-08-23 12:44:50 +08:00
debug = get_debug_flag()
2014-04-28 13:26:23 +02:00
if reload is None:
reload = debug
2014-04-28 13:26:23 +02:00
if debugger is None:
debugger = debug
2022-08-01 15:11:21 -07:00
show_server_banner(debug, info.app_import_path)
run_simple(
host,
port,
app,
use_reloader=reload,
use_debugger=debugger,
threaded=with_threads,
ssl_context=cert,
extra_files=extra_files,
exclude_patterns=exclude_patterns,
)
2014-04-28 13:26:23 +02:00
2022-08-23 12:44:50 +08:00
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:
2018-12-01 09:01:12 +08:00
"""Run an interactive Python shell in the context of a given
2014-04-28 13:26:23 +02:00
Flask application. The application will populate the default
2020-08-26 09:20:21 -07:00
namespace of this shell according to its configuration.
2014-04-28 13:26:23 +02:00
This is useful for executing small snippets of management code
2017-10-24 01:10:12 -05:00
without having to manually configure the application.
2014-04-28 13:26:23 +02:00
"""
import code
2020-04-04 11:39:03 -07:00
banner = (
f"Python {sys.version} on {sys.platform}\n"
2022-08-05 11:29:39 -07:00
f"App: {current_app.import_name}\n"
2022-07-05 06:33:03 -07:00
f"Instance: {current_app.instance_path}"
2014-04-28 13:26:23 +02:00
)
2023-12-14 08:58:13 -08:00
ctx: dict[str, t.Any] = {}
# Support the regular Python interpreter startup script if someone
# is using it.
startup = os.environ.get("PYTHONSTARTUP")
if startup and os.path.isfile(startup):
2020-04-04 09:43:06 -07:00
with open(startup) as f:
eval(compile(f.read(), startup, "exec"), ctx)
2022-07-05 06:33:03 -07:00
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
# tab and history completion.
interactive_hook = getattr(sys, "__interactivehook__", None)
if interactive_hook is not None:
try:
import readline
from rlcompleter import Completer
except ImportError:
pass
else:
# rlcompleter uses __main__.__dict__ by default, which is
# flask.__main__. Use the shell context instead.
readline.set_completer(Completer(ctx).complete)
interactive_hook()
code.interact(banner=banner, local=ctx)
2014-04-28 13:26:23 +02:00
@click.command("routes", short_help="Show the routes for the app.")
@click.option(
"--sort",
"-s",
type=click.Choice(("endpoint", "methods", "domain", "rule", "match")),
default="endpoint",
help=(
"Method to sort routes by. 'match' is the order that Flask will match routes"
" when dispatching a request."
),
)
@click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.")
@with_appcontext
def routes_command(sort: str, all_methods: bool) -> None:
"""Show all registered routes with endpoints and methods."""
rules = list(current_app.url_map.iter_rules())
2018-05-02 17:08:31 +08:00
if not rules:
click.echo("No routes were registered.")
2018-05-02 17:08:31 +08:00
return
ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"}
host_matching = current_app.url_map.host_matching
has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules)
rows = []
for rule in rules:
row = [
rule.endpoint,
", ".join(sorted((rule.methods or set()) - ignored_methods)),
]
if has_domain:
row.append((rule.host if host_matching else rule.subdomain) or "")
row.append(rule.rule)
rows.append(row)
headers = ["Endpoint", "Methods"]
sorts = ["endpoint", "methods"]
if has_domain:
headers.append("Host" if host_matching else "Subdomain")
sorts.append("domain")
headers.append("Rule")
sorts.append("rule")
try:
rows.sort(key=itemgetter(sorts.index(sort)))
except ValueError:
pass
rows.insert(0, headers)
widths = [max(len(row[i]) for row in rows) for i in range(len(headers))]
rows.insert(1, ["-" * w for w in widths])
template = " ".join(f"{{{i}:<{w}}}" for i, w in enumerate(widths))
for row in rows:
click.echo(template.format(*row))
cli = FlaskGroup(
2022-06-15 14:07:00 -07:00
name="flask",
help="""\
A general utility script for Flask applications.
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.
""",
)
def main() -> None:
cli.main()
2014-04-28 13:26:23 +02:00
if __name__ == "__main__":
main()