1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051 |
- from __future__ import annotations
- import ast
- import inspect
- import os
- import platform
- import re
- import sys
- import traceback
- import typing as t
- from functools import update_wrapper
- from operator import attrgetter
- 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_load_dotenv
- if t.TYPE_CHECKING:
- from .app import Flask
- class NoAppException(click.UsageError):
- """Raised if an application cannot be found or loaded."""
- def find_best_app(module):
- """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"):
- app = getattr(module, attr_name, None)
- if isinstance(app, Flask):
- return app
- # Otherwise find the only object that is a Flask instance.
- matches = [v for v in module.__dict__.values() if isinstance(v, Flask)]
- if len(matches) == 1:
- return matches[0]
- elif len(matches) > 1:
- raise NoAppException(
- "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):
- raise
- raise NoAppException(
- f"Detected factory '{attr_name}' in module '{module.__name__}',"
- " but could not call it without arguments. Use"
- 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__}'. Use '{module.__name__}:name'"
- " to specify one."
- )
- def _called_with_wrong_args(f):
- """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.
- # https://docs.python.org/2/library/sys.html#sys.exc_info
- del tb
- def find_app_by_string(module, app_name):
- """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]
- kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expr.keywords}
- 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):
- raise
- raise NoAppException(
- f"The factory {app_name!r} in module"
- 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(
- "A valid Flask application was not obtained from"
- f" '{module.__name__}:{app_name}'."
- )
- def prepare_import(path):
- """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)
- 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")):
- break
- if sys.path[0] != path:
- sys.path.insert(0, path)
- return ".".join(module_name[::-1])
- def locate_app(module_name, app_name, raise_if_not_found=True):
- 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:
- raise NoAppException(
- f"While importing {module_name!r}, an ImportError was"
- f" raised:\n\n{traceback.format_exc()}"
- ) from None
- elif raise_if_not_found:
- raise NoAppException(f"Could not import {module_name!r}.") from None
- else:
- return
- module = sys.modules[module_name]
- if app_name is None:
- return find_best_app(module)
- else:
- return find_app_by_string(module, app_name)
- def get_version(ctx, param, value):
- if not value or ctx.resilient_parsing:
- return
- import werkzeug
- from . import __version__
- click.echo(
- f"Python {platform.python_version()}\n"
- f"Flask {__version__}\n"
- f"Werkzeug {werkzeug.__version__}",
- color=ctx.color,
- )
- ctx.exit()
- version_option = click.Option(
- ["--version"],
- help="Show the Flask version.",
- expose_value=False,
- callback=get_version,
- is_flag=True,
- is_eager=True,
- )
- 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
- 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.
- """
- 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
- #: 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: t.Dict[t.Any, t.Any] = {}
- self.set_debug_flag = set_debug_flag
- self._loaded_app: Flask | None = None
- 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.
- """
- if self._loaded_app is not None:
- return self._loaded_app
- if self.create_app is not None:
- app = self.create_app()
- else:
- if self.app_import_path:
- path, name = (
- re.split(r":(?![\\/])", self.app_import_path, 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)
- if app:
- break
- if not app:
- 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
- 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.
- 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):
- 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)
- 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`.
- """
- def command(self, *args, **kwargs):
- """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):
- if wrap_for_ctx:
- f = with_appcontext(f)
- return click.Group.command(self, *args, **kwargs)(f)
- return decorator
- def group(self, *args, **kwargs):
- """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)
- 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
- 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
- shell commands will be added.
- :param add_version_option: adds the ``--version`` option.
- :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.
- :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
- 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:
- 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)
- 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
- if add_default_commands:
- self.add_command(run_command)
- self.add_command(shell_command)
- self.add_command(routes_command)
- self._loaded_plugin_commands = False
- def _load_plugin_commands(self):
- 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.
- import importlib_metadata as metadata
- for ep in metadata.entry_points(group="flask.commands"):
- self.add_command(ep.load(), ep.name)
- self._loaded_plugin_commands = True
- def get_command(self, ctx, name):
- 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)
- 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.
- 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.
- 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()
- # 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.
- 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")
- return sorted(rv)
- 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()
- 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
- )
- 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):
- """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:
- """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
- list are preferred over later files.
- This is a no-op if `python-dotenv`_ is not installed.
- .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme
- :param path: Load the file at this location instead of searching.
- :return: ``True`` if a file was loaded.
- .. 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:
- import dotenv
- except ImportError:
- if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"):
- click.secho(
- " * Tip: There are .env or .flaskenv files present."
- ' Do "pip install python-dotenv" to use them.',
- fg="yellow",
- err=True,
- )
- return 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
- loaded = False
- for name in (".env", ".flaskenv"):
- path = dotenv.find_dotenv(name, usecwd=True)
- if not path:
- continue
- dotenv.load_dotenv(path, encoding="utf-8")
- loaded = True
- return loaded # True if at least one file was located and loaded.
- def show_server_banner(debug, app_import_path):
- """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:
- 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"
- def __init__(self):
- self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True)
- def convert(self, value, param, ctx):
- try:
- import ssl
- except ImportError:
- raise click.BadParameter(
- 'Using "--cert" requires Python to be compiled with SSL support.',
- ctx,
- param,
- ) from None
- try:
- return self.path_type(value, param, ctx)
- except click.BadParameter:
- value = click.STRING(value, param, ctx).lower()
- if value == "adhoc":
- try:
- import cryptography # noqa: F401
- except ImportError:
- raise click.BadParameter(
- "Using ad-hoc certificates requires the cryptography library.",
- ctx,
- param,
- ) from None
- return value
- obj = import_string(value, silent=True)
- if isinstance(obj, ssl.SSLContext):
- return obj
- raise
- def _validate_key(ctx, param, value):
- """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.
- """
- def convert(self, value, param, ctx):
- items = self.split_envvar_value(value)
- 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(
- "--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"
- 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}."
- ),
- )
- @pass_script_info
- def run_command(
- info,
- host,
- port,
- reload,
- debugger,
- with_threads,
- cert,
- extra_files,
- exclude_patterns,
- ):
- """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.
- 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:
- reload = debug
- if debugger is None:
- debugger = debug
- 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,
- )
- @click.command("shell", short_help="Run a shell in the app context.")
- @with_appcontext
- def shell_command() -> None:
- """Run an interactive Python shell in the context of a given
- Flask application. The application will populate the default
- namespace of this shell according to its configuration.
- This is useful for executing small snippets of management code
- without having to manually configure the application.
- """
- import code
- banner = (
- f"Python {sys.version} on {sys.platform}\n"
- f"App: {current_app.import_name}\n"
- f"Instance: {current_app.instance_path}"
- )
- ctx: dict = {}
- # Support the regular Python interpreter startup script if someone
- # is using it.
- startup = os.environ.get("PYTHONSTARTUP")
- if startup and os.path.isfile(startup):
- with open(startup) as f:
- eval(compile(f.read(), startup, "exec"), ctx)
- 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)
- @click.command("routes", short_help="Show the routes for the app.")
- @click.option(
- "--sort",
- "-s",
- type=click.Choice(("endpoint", "methods", "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())
- if not rules:
- click.echo("No routes were registered.")
- return
- ignored_methods = set(() if all_methods else ("HEAD", "OPTIONS"))
- if sort in ("endpoint", "rule"):
- rules = sorted(rules, key=attrgetter(sort))
- elif sort == "methods":
- rules = sorted(rules, key=lambda rule: sorted(rule.methods)) # type: ignore
- rule_methods = [
- ", ".join(sorted(rule.methods - ignored_methods)) # type: ignore
- for rule in rules
- ]
- headers = ("Endpoint", "Methods", "Rule")
- widths = (
- max(len(rule.endpoint) for rule in rules),
- max(len(methods) for methods in rule_methods),
- max(len(rule.rule) for rule in rules),
- )
- widths = [max(len(h), w) for h, w in zip(headers, widths)]
- row = "{{0:<{0}}} {{1:<{1}}} {{2:<{2}}}".format(*widths)
- click.echo(row.format(*headers).strip())
- click.echo(row.format(*("-" * width for width in widths)))
- for rule, methods in zip(rules, rule_methods):
- click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip())
- cli = FlaskGroup(
- 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()
- if __name__ == "__main__":
- main()
|