This commit is contained in:
2025-05-22 21:22:15 +02:00
parent 3d57f842f9
commit 97cb9c8703
156 changed files with 1205 additions and 6603 deletions

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import ast
import collections.abc as cabc
import importlib.metadata
import inspect
import os
@@ -11,6 +12,7 @@ import traceback
import typing as t
from functools import update_wrapper
from operator import itemgetter
from types import ModuleType
import click
from click.core import ParameterSource
@@ -23,6 +25,12 @@ from .helpers import get_debug_flag
from .helpers import get_load_dotenv
if t.TYPE_CHECKING:
import ssl
from _typeshed.wsgi import StartResponse
from _typeshed.wsgi import WSGIApplication
from _typeshed.wsgi import WSGIEnvironment
from .app import Flask
@@ -30,7 +38,7 @@ class NoAppException(click.UsageError):
"""Raised if an application cannot be found or loaded."""
def find_best_app(module):
def find_best_app(module: ModuleType) -> Flask:
"""Given a module instance this tries to find the best possible
application in the module or raises an exception.
"""
@@ -83,7 +91,7 @@ def find_best_app(module):
)
def _called_with_wrong_args(f):
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.
@@ -109,7 +117,7 @@ def _called_with_wrong_args(f):
del tb
def find_app_by_string(module, app_name):
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.
"""
@@ -140,7 +148,11 @@ def find_app_by_string(module, app_name):
# Parse the positional and keyword arguments as literals.
try:
args = [ast.literal_eval(arg) for arg in expr.args]
kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expr.keywords}
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.
@@ -185,7 +197,7 @@ def find_app_by_string(module, app_name):
)
def prepare_import(path):
def prepare_import(path: str) -> str:
"""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.
"""
@@ -214,13 +226,29 @@ def prepare_import(path):
return ".".join(module_name[::-1])
def locate_app(module_name, app_name, raise_if_not_found=True):
@t.overload
def locate_app(
module_name: str, app_name: str | None, raise_if_not_found: t.Literal[True] = True
) -> Flask:
...
@t.overload
def locate_app(
module_name: str, app_name: str | None, raise_if_not_found: t.Literal[False] = ...
) -> Flask | None:
...
def locate_app(
module_name: str, app_name: str | None, raise_if_not_found: bool = True
) -> Flask | None:
try:
__import__(module_name)
except ImportError:
# Reraise the ImportError if it occurred within the imported module.
# Determine this by checking whether the trace has a depth > 1.
if sys.exc_info()[2].tb_next:
if sys.exc_info()[2].tb_next: # type: ignore[union-attr]
raise NoAppException(
f"While importing {module_name!r}, an ImportError was"
f" raised:\n\n{traceback.format_exc()}"
@@ -228,7 +256,7 @@ def locate_app(module_name, app_name, raise_if_not_found=True):
elif raise_if_not_found:
raise NoAppException(f"Could not import {module_name!r}.") from None
else:
return
return None
module = sys.modules[module_name]
@@ -238,7 +266,7 @@ def locate_app(module_name, app_name, raise_if_not_found=True):
return find_app_by_string(module, app_name)
def get_version(ctx, param, value):
def get_version(ctx: click.Context, param: click.Parameter, value: t.Any) -> None:
if not value or ctx.resilient_parsing:
return
@@ -299,7 +327,7 @@ class ScriptInfo:
return self._loaded_app
if self.create_app is not None:
app = self.create_app()
app: Flask | None = self.create_app()
else:
if self.app_import_path:
path, name = (
@@ -312,10 +340,10 @@ class ScriptInfo:
import_name = prepare_import(path)
app = locate_app(import_name, None, raise_if_not_found=False)
if app:
if app is not None:
break
if not app:
if app is None:
raise NoAppException(
"Could not locate a Flask application. Use the"
" 'flask --app' option, 'FLASK_APP' environment"
@@ -334,8 +362,10 @@ class ScriptInfo:
pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True)
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
def with_appcontext(f):
def with_appcontext(f: F) -> F:
"""Wraps a callback so that it's guaranteed to be executed with the
script's application context.
@@ -350,14 +380,14 @@ def with_appcontext(f):
"""
@click.pass_context
def decorator(__ctx, *args, **kwargs):
def decorator(ctx: click.Context, /, *args: t.Any, **kwargs: t.Any) -> t.Any:
if not current_app:
app = __ctx.ensure_object(ScriptInfo).load_app()
__ctx.with_resource(app.app_context())
app = ctx.ensure_object(ScriptInfo).load_app()
ctx.with_resource(app.app_context())
return __ctx.invoke(f, *args, **kwargs)
return ctx.invoke(f, *args, **kwargs)
return update_wrapper(decorator, f)
return update_wrapper(decorator, f) # type: ignore[return-value]
class AppGroup(click.Group):
@@ -368,27 +398,31 @@ class AppGroup(click.Group):
Not to be confused with :class:`FlaskGroup`.
"""
def command(self, *args, **kwargs):
def command( # type: ignore[override]
self, *args: t.Any, **kwargs: t.Any
) -> t.Callable[[t.Callable[..., t.Any]], click.Command]:
"""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)
def decorator(f):
def decorator(f: t.Callable[..., t.Any]) -> click.Command:
if wrap_for_ctx:
f = with_appcontext(f)
return click.Group.command(self, *args, **kwargs)(f)
return super(AppGroup, self).command(*args, **kwargs)(f) # type: ignore[no-any-return]
return decorator
def group(self, *args, **kwargs):
def group( # type: ignore[override]
self, *args: t.Any, **kwargs: t.Any
) -> t.Callable[[t.Callable[..., t.Any]], click.Group]:
"""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)
return click.Group.group(self, *args, **kwargs)
return super().group(*args, **kwargs) # type: ignore[no-any-return]
def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None:
@@ -545,7 +579,7 @@ class FlaskGroup(AppGroup):
self._loaded_plugin_commands = False
def _load_plugin_commands(self):
def _load_plugin_commands(self) -> None:
if self._loaded_plugin_commands:
return
@@ -562,7 +596,7 @@ class FlaskGroup(AppGroup):
self._loaded_plugin_commands = True
def get_command(self, ctx, name):
def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
self._load_plugin_commands()
# Look up built-in and plugin commands, which should be
# available even if the app fails to load.
@@ -584,12 +618,12 @@ class FlaskGroup(AppGroup):
# 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:
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)
def list_commands(self, ctx):
def list_commands(self, ctx: click.Context) -> list[str]:
self._load_plugin_commands()
# Start with the built-in and plugin commands.
rv = set(super().list_commands(ctx))
@@ -645,14 +679,14 @@ class FlaskGroup(AppGroup):
return super().parse_args(ctx, args)
def _path_is_ancestor(path, other):
def _path_is_ancestor(path: str, other: str) -> bool:
"""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
def load_dotenv(path: str | os.PathLike | None = None) -> bool:
def load_dotenv(path: str | os.PathLike[str] | 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
@@ -713,7 +747,7 @@ def load_dotenv(path: str | os.PathLike | None = None) -> bool:
return loaded # True if at least one file was located and loaded.
def show_server_banner(debug, app_import_path):
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.
"""
@@ -735,10 +769,12 @@ class CertParamType(click.ParamType):
name = "path"
def __init__(self):
def __init__(self) -> None:
self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True)
def convert(self, value, param, ctx):
def convert(
self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None
) -> t.Any:
try:
import ssl
except ImportError:
@@ -773,7 +809,7 @@ class CertParamType(click.ParamType):
raise
def _validate_key(ctx, param, value):
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.
"""
@@ -795,7 +831,9 @@ def _validate_key(ctx, param, value):
if is_context:
raise click.BadParameter(
'When "--cert" is an SSLContext object, "--key is not used.', ctx, param
'When "--cert" is an SSLContext object, "--key" is not used.',
ctx,
param,
)
if not cert:
@@ -816,8 +854,11 @@ class SeparatedPathType(click.Path):
validated as a :class:`click.Path` type.
"""
def convert(self, value, param, ctx):
def convert(
self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None
) -> t.Any:
items = self.split_envvar_value(value)
# 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]
@@ -876,16 +917,16 @@ class SeparatedPathType(click.Path):
)
@pass_script_info
def run_command(
info,
host,
port,
reload,
debugger,
with_threads,
cert,
extra_files,
exclude_patterns,
):
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
@@ -895,7 +936,7 @@ def run_command(
option.
"""
try:
app = info.load_app()
app: WSGIApplication = info.load_app()
except Exception as e:
if is_running_from_reloader():
# When reloading, print out the error immediately, but raise
@@ -903,7 +944,9 @@ def run_command(
traceback.print_exc()
err = e
def app(environ, start_response):
def app(
environ: WSGIEnvironment, start_response: StartResponse
) -> cabc.Iterable[bytes]:
raise err from None
else:
@@ -954,7 +997,7 @@ def shell_command() -> None:
f"App: {current_app.import_name}\n"
f"Instance: {current_app.instance_path}"
)
ctx: dict = {}
ctx: dict[str, t.Any] = {}
# Support the regular Python interpreter startup script if someone
# is using it.