Format: text/x-python
⬇ Download File (36.2 KB)
   1  from __future__ import annotations
   2  
   3  import ast
   4  import collections.abc as cabc
   5  import importlib.metadata
   6  import inspect
   7  import os
   8  import platform
   9  import re
  10  import sys
  11  import traceback
  12  import typing as t
  13  from functools import update_wrapper
  14  from operator import itemgetter
  15  from types import ModuleType
  16  
  17  import click
  18  from click.core import ParameterSource
  19  from werkzeug import run_simple
  20  from werkzeug.serving import is_running_from_reloader
  21  from werkzeug.utils import import_string
  22  
  23  from .globals import current_app
  24  from .helpers import get_debug_flag
  25  from .helpers import get_load_dotenv
  26  
  27  if t.TYPE_CHECKING:
  28      import ssl
  29  
  30      from _typeshed.wsgi import StartResponse
  31      from _typeshed.wsgi import WSGIApplication
  32      from _typeshed.wsgi import WSGIEnvironment
  33  
  34      from .app import Flask
  35  
  36  
  37  class NoAppException(click.UsageError):
  38      """Raised if an application cannot be found or loaded."""
  39  
  40  
  41  def find_best_app(module: ModuleType) -> Flask:
  42      """Given a module instance this tries to find the best possible
  43      application in the module or raises an exception.
  44      """
  45      from . import Flask
  46  
  47      # Search for the most common names first.
  48      for attr_name in ("app", "application"):
  49          app = getattr(module, attr_name, None)
  50  
  51          if isinstance(app, Flask):
  52              return app
  53  
  54      # Otherwise find the only object that is a Flask instance.
  55      matches = [v for v in module.__dict__.values() if isinstance(v, Flask)]
  56  
  57      if len(matches) == 1:
  58          return matches[0]
  59      elif len(matches) > 1:
  60          raise NoAppException(
  61              "Detected multiple Flask applications in module"
  62              f" '{module.__name__}'. Use '{module.__name__}:name'"
  63              " to specify the correct one."
  64          )
  65  
  66      # Search for app factory functions.
  67      for attr_name in ("create_app", "make_app"):
  68          app_factory = getattr(module, attr_name, None)
  69  
  70          if inspect.isfunction(app_factory):
  71              try:
  72                  app = app_factory()
  73  
  74                  if isinstance(app, Flask):
  75                      return app
  76              except TypeError as e:
  77                  if not _called_with_wrong_args(app_factory):
  78                      raise
  79  
  80                  raise NoAppException(
  81                      f"Detected factory '{attr_name}' in module '{module.__name__}',"
  82                      " but could not call it without arguments. Use"
  83                      f" '{module.__name__}:{attr_name}(args)'"
  84                      " to specify arguments."
  85                  ) from e
  86  
  87      raise NoAppException(
  88          "Failed to find Flask application or factory in module"
  89          f" '{module.__name__}'. Use '{module.__name__}:name'"
  90          " to specify one."
  91      )
  92  
  93  
  94  def _called_with_wrong_args(f: t.Callable[..., Flask]) -> bool:
  95      """Check whether calling a function raised a ``TypeError`` because
  96      the call failed or because something in the factory raised the
  97      error.
  98  
  99      :param f: The function that was called.
 100      :return: ``True`` if the call failed.
 101      """
 102      tb = sys.exc_info()[2]
 103  
 104      try:
 105          while tb is not None:
 106              if tb.tb_frame.f_code is f.__code__:
 107                  # In the function, it was called successfully.
 108                  return False
 109  
 110              tb = tb.tb_next
 111  
 112          # Didn't reach the function.
 113          return True
 114      finally:
 115          # Delete tb to break a circular reference.
 116          # https://docs.python.org/2/library/sys.html#sys.exc_info
 117          del tb
 118  
 119  
 120  def find_app_by_string(module: ModuleType, app_name: str) -> Flask:
 121      """Check if the given string is a variable name or a function. Call
 122      a function to get the app instance, or return the variable directly.
 123      """
 124      from . import Flask
 125  
 126      # Parse app_name as a single expression to determine if it's a valid
 127      # attribute name or function call.
 128      try:
 129          expr = ast.parse(app_name.strip(), mode="eval").body
 130      except SyntaxError:
 131          raise NoAppException(
 132              f"Failed to parse {app_name!r} as an attribute name or function call."
 133          ) from None
 134  
 135      if isinstance(expr, ast.Name):
 136          name = expr.id
 137          args = []
 138          kwargs = {}
 139      elif isinstance(expr, ast.Call):
 140          # Ensure the function name is an attribute name only.
 141          if not isinstance(expr.func, ast.Name):
 142              raise NoAppException(
 143                  f"Function reference must be a simple name: {app_name!r}."
 144              )
 145  
 146          name = expr.func.id
 147  
 148          # Parse the positional and keyword arguments as literals.
 149          try:
 150              args = [ast.literal_eval(arg) for arg in expr.args]
 151              kwargs = {
 152                  kw.arg: ast.literal_eval(kw.value)
 153                  for kw in expr.keywords
 154                  if kw.arg is not None
 155              }
 156          except ValueError:
 157              # literal_eval gives cryptic error messages, show a generic
 158              # message with the full expression instead.
 159              raise NoAppException(
 160                  f"Failed to parse arguments as literal values: {app_name!r}."
 161              ) from None
 162      else:
 163          raise NoAppException(
 164              f"Failed to parse {app_name!r} as an attribute name or function call."
 165          )
 166  
 167      try:
 168          attr = getattr(module, name)
 169      except AttributeError as e:
 170          raise NoAppException(
 171              f"Failed to find attribute {name!r} in {module.__name__!r}."
 172          ) from e
 173  
 174      # If the attribute is a function, call it with any args and kwargs
 175      # to get the real application.
 176      if inspect.isfunction(attr):
 177          try:
 178              app = attr(*args, **kwargs)
 179          except TypeError as e:
 180              if not _called_with_wrong_args(attr):
 181                  raise
 182  
 183              raise NoAppException(
 184                  f"The factory {app_name!r} in module"
 185                  f" {module.__name__!r} could not be called with the"
 186                  " specified arguments."
 187              ) from e
 188      else:
 189          app = attr
 190  
 191      if isinstance(app, Flask):
 192          return app
 193  
 194      raise NoAppException(
 195          "A valid Flask application was not obtained from"
 196          f" '{module.__name__}:{app_name}'."
 197      )
 198  
 199  
 200  def prepare_import(path: str) -> str:
 201      """Given a filename this will try to calculate the python path, add it
 202      to the search path and return the actual module name that is expected.
 203      """
 204      path = os.path.realpath(path)
 205  
 206      fname, ext = os.path.splitext(path)
 207      if ext == ".py":
 208          path = fname
 209  
 210      if os.path.basename(path) == "__init__":
 211          path = os.path.dirname(path)
 212  
 213      module_name = []
 214  
 215      # move up until outside package structure (no __init__.py)
 216      while True:
 217          path, name = os.path.split(path)
 218          module_name.append(name)
 219  
 220          if not os.path.exists(os.path.join(path, "__init__.py")):
 221              break
 222  
 223      if sys.path[0] != path:
 224          sys.path.insert(0, path)
 225  
 226      return ".".join(module_name[::-1])
 227  
 228  
 229  @t.overload
 230  def locate_app(
 231      module_name: str, app_name: str | None, raise_if_not_found: t.Literal[True] = True
 232  ) -> Flask: ...
 233  
 234  
 235  @t.overload
 236  def locate_app(
 237      module_name: str, app_name: str | None, raise_if_not_found: t.Literal[False] = ...
 238  ) -> Flask | None: ...
 239  
 240  
 241  def locate_app(
 242      module_name: str, app_name: str | None, raise_if_not_found: bool = True
 243  ) -> Flask | None:
 244      try:
 245          __import__(module_name)
 246      except ImportError:
 247          # Reraise the ImportError if it occurred within the imported module.
 248          # Determine this by checking whether the trace has a depth > 1.
 249          if sys.exc_info()[2].tb_next:  # type: ignore[union-attr]
 250              raise NoAppException(
 251                  f"While importing {module_name!r}, an ImportError was"
 252                  f" raised:\n\n{traceback.format_exc()}"
 253              ) from None
 254          elif raise_if_not_found:
 255              raise NoAppException(f"Could not import {module_name!r}.") from None
 256          else:
 257              return None
 258  
 259      module = sys.modules[module_name]
 260  
 261      if app_name is None:
 262          return find_best_app(module)
 263      else:
 264          return find_app_by_string(module, app_name)
 265  
 266  
 267  def get_version(ctx: click.Context, param: click.Parameter, value: t.Any) -> None:
 268      if not value or ctx.resilient_parsing:
 269          return
 270  
 271      flask_version = importlib.metadata.version("flask")
 272      werkzeug_version = importlib.metadata.version("werkzeug")
 273  
 274      click.echo(
 275          f"Python {platform.python_version()}\n"
 276          f"Flask {flask_version}\n"
 277          f"Werkzeug {werkzeug_version}",
 278          color=ctx.color,
 279      )
 280      ctx.exit()
 281  
 282  
 283  version_option = click.Option(
 284      ["--version"],
 285      help="Show the Flask version.",
 286      expose_value=False,
 287      callback=get_version,
 288      is_flag=True,
 289      is_eager=True,
 290  )
 291  
 292  
 293  class ScriptInfo:
 294      """Helper object to deal with Flask applications.  This is usually not
 295      necessary to interface with as it's used internally in the dispatching
 296      to click.  In future versions of Flask this object will most likely play
 297      a bigger role.  Typically it's created automatically by the
 298      :class:`FlaskGroup` but you can also manually create it and pass it
 299      onwards as click object.
 300  
 301      .. versionchanged:: 3.1
 302          Added the ``load_dotenv_defaults`` parameter and attribute.
 303      """
 304  
 305      def __init__(
 306          self,
 307          app_import_path: str | None = None,
 308          create_app: t.Callable[..., Flask] | None = None,
 309          set_debug_flag: bool = True,
 310          load_dotenv_defaults: bool = True,
 311      ) -> None:
 312          #: Optionally the import path for the Flask application.
 313          self.app_import_path = app_import_path
 314          #: Optionally a function that is passed the script info to create
 315          #: the instance of the application.
 316          self.create_app = create_app
 317          #: A dictionary with arbitrary data that can be associated with
 318          #: this script info.
 319          self.data: dict[t.Any, t.Any] = {}
 320          self.set_debug_flag = set_debug_flag
 321  
 322          self.load_dotenv_defaults = get_load_dotenv(load_dotenv_defaults)
 323          """Whether default ``.flaskenv`` and ``.env`` files should be loaded.
 324  
 325          ``ScriptInfo`` doesn't load anything, this is for reference when doing
 326          the load elsewhere during processing.
 327  
 328          .. versionadded:: 3.1
 329          """
 330  
 331          self._loaded_app: Flask | None = None
 332  
 333      def load_app(self) -> Flask:
 334          """Loads the Flask app (if not yet loaded) and returns it.  Calling
 335          this multiple times will just result in the already loaded app to
 336          be returned.
 337          """
 338          if self._loaded_app is not None:
 339              return self._loaded_app
 340          app: Flask | None = None
 341          if self.create_app is not None:
 342              app = self.create_app()
 343          else:
 344              if self.app_import_path:
 345                  path, name = (
 346                      re.split(r":(?![\\/])", self.app_import_path, maxsplit=1) + [None]
 347                  )[:2]
 348                  import_name = prepare_import(path)
 349                  app = locate_app(import_name, name)
 350              else:
 351                  for path in ("wsgi.py", "app.py"):
 352                      import_name = prepare_import(path)
 353                      app = locate_app(import_name, None, raise_if_not_found=False)
 354  
 355                      if app is not None:
 356                          break
 357  
 358          if app is None:
 359              raise NoAppException(
 360                  "Could not locate a Flask application. Use the"
 361                  " 'flask --app' option, 'FLASK_APP' environment"
 362                  " variable, or a 'wsgi.py' or 'app.py' file in the"
 363                  " current directory."
 364              )
 365  
 366          if self.set_debug_flag:
 367              # Update the app's debug flag through the descriptor so that
 368              # other values repopulate as well.
 369              app.debug = get_debug_flag()
 370  
 371          self._loaded_app = app
 372          return app
 373  
 374  
 375  pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True)
 376  
 377  F = t.TypeVar("F", bound=t.Callable[..., t.Any])
 378  
 379  
 380  def with_appcontext(f: F) -> F:
 381      """Wraps a callback so that it's guaranteed to be executed with the
 382      script's application context.
 383  
 384      Custom commands (and their options) registered under ``app.cli`` or
 385      ``blueprint.cli`` will always have an app context available, this
 386      decorator is not required in that case.
 387  
 388      .. versionchanged:: 2.2
 389          The app context is active for subcommands as well as the
 390          decorated callback. The app context is always available to
 391          ``app.cli`` command and parameter callbacks.
 392      """
 393  
 394      @click.pass_context
 395      def decorator(ctx: click.Context, /, *args: t.Any, **kwargs: t.Any) -> t.Any:
 396          if not current_app:
 397              app = ctx.ensure_object(ScriptInfo).load_app()
 398              ctx.with_resource(app.app_context())
 399  
 400          return ctx.invoke(f, *args, **kwargs)
 401  
 402      return update_wrapper(decorator, f)  # type: ignore[return-value]
 403  
 404  
 405  class AppGroup(click.Group):
 406      """This works similar to a regular click :class:`~click.Group` but it
 407      changes the behavior of the :meth:`command` decorator so that it
 408      automatically wraps the functions in :func:`with_appcontext`.
 409  
 410      Not to be confused with :class:`FlaskGroup`.
 411      """
 412  
 413      def command(  # type: ignore[override]
 414          self, *args: t.Any, **kwargs: t.Any
 415      ) -> t.Callable[[t.Callable[..., t.Any]], click.Command]:
 416          """This works exactly like the method of the same name on a regular
 417          :class:`click.Group` but it wraps callbacks in :func:`with_appcontext`
 418          unless it's disabled by passing ``with_appcontext=False``.
 419          """
 420          wrap_for_ctx = kwargs.pop("with_appcontext", True)
 421  
 422          def decorator(f: t.Callable[..., t.Any]) -> click.Command:
 423              if wrap_for_ctx:
 424                  f = with_appcontext(f)
 425              return super(AppGroup, self).command(*args, **kwargs)(f)  # type: ignore[no-any-return]
 426  
 427          return decorator
 428  
 429      def group(  # type: ignore[override]
 430          self, *args: t.Any, **kwargs: t.Any
 431      ) -> t.Callable[[t.Callable[..., t.Any]], click.Group]:
 432          """This works exactly like the method of the same name on a regular
 433          :class:`click.Group` but it defaults the group class to
 434          :class:`AppGroup`.
 435          """
 436          kwargs.setdefault("cls", AppGroup)
 437          return super().group(*args, **kwargs)  # type: ignore[no-any-return]
 438  
 439  
 440  def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None:
 441      if value is None:
 442          return None
 443  
 444      info = ctx.ensure_object(ScriptInfo)
 445      info.app_import_path = value
 446      return value
 447  
 448  
 449  # This option is eager so the app will be available if --help is given.
 450  # --help is also eager, so --app must be before it in the param list.
 451  # no_args_is_help bypasses eager processing, so this option must be
 452  # processed manually in that case to ensure FLASK_APP gets picked up.
 453  _app_option = click.Option(
 454      ["-A", "--app"],
 455      metavar="IMPORT",
 456      help=(
 457          "The Flask application or factory function to load, in the form 'module:name'."
 458          " Module can be a dotted import or file path. Name is not required if it is"
 459          " 'app', 'application', 'create_app', or 'make_app', and can be 'name(args)' to"
 460          " pass arguments."
 461      ),
 462      is_eager=True,
 463      expose_value=False,
 464      callback=_set_app,
 465  )
 466  
 467  
 468  def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None:
 469      # If the flag isn't provided, it will default to False. Don't use
 470      # that, let debug be set by env in that case.
 471      source = ctx.get_parameter_source(param.name)  # type: ignore[arg-type]
 472  
 473      if source is not None and source in (
 474          ParameterSource.DEFAULT,
 475          ParameterSource.DEFAULT_MAP,
 476      ):
 477          return None
 478  
 479      # Set with env var instead of ScriptInfo.load so that it can be
 480      # accessed early during a factory function.
 481      os.environ["FLASK_DEBUG"] = "1" if value else "0"
 482      return value
 483  
 484  
 485  _debug_option = click.Option(
 486      ["--debug/--no-debug"],
 487      help="Set debug mode.",
 488      expose_value=False,
 489      callback=_set_debug,
 490  )
 491  
 492  
 493  def _env_file_callback(
 494      ctx: click.Context, param: click.Option, value: str | None
 495  ) -> str | None:
 496      try:
 497          import dotenv  # noqa: F401
 498      except ImportError:
 499          # Only show an error if a value was passed, otherwise we still want to
 500          # call load_dotenv and show a message without exiting.
 501          if value is not None:
 502              raise click.BadParameter(
 503                  "python-dotenv must be installed to load an env file.",
 504                  ctx=ctx,
 505                  param=param,
 506              ) from None
 507  
 508      # Load if a value was passed, or we want to load default files, or both.
 509      if value is not None or ctx.obj.load_dotenv_defaults:
 510          load_dotenv(value, load_defaults=ctx.obj.load_dotenv_defaults)
 511  
 512      return value
 513  
 514  
 515  # This option is eager so env vars are loaded as early as possible to be
 516  # used by other options.
 517  _env_file_option = click.Option(
 518      ["-e", "--env-file"],
 519      type=click.Path(exists=True, dir_okay=False),
 520      help=(
 521          "Load environment variables from this file, taking precedence over"
 522          " those set by '.env' and '.flaskenv'. Variables set directly in the"
 523          " environment take highest precedence. python-dotenv must be installed."
 524      ),
 525      is_eager=True,
 526      expose_value=False,
 527      callback=_env_file_callback,
 528  )
 529  
 530  
 531  class FlaskGroup(AppGroup):
 532      """Special subclass of the :class:`AppGroup` group that supports
 533      loading more commands from the configured Flask app.  Normally a
 534      developer does not have to interface with this class but there are
 535      some very advanced use cases for which it makes sense to create an
 536      instance of this. see :ref:`custom-scripts`.
 537  
 538      :param add_default_commands: if this is True then the default run and
 539          shell commands will be added.
 540      :param add_version_option: adds the ``--version`` option.
 541      :param create_app: an optional callback that is passed the script info and
 542          returns the loaded app.
 543      :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv`
 544          files to set environment variables. Will also change the working
 545          directory to the directory containing the first file found.
 546      :param set_debug_flag: Set the app's debug flag.
 547  
 548      .. versionchanged:: 3.1
 549          ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files.
 550  
 551      .. versionchanged:: 2.2
 552          Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options.
 553  
 554      .. versionchanged:: 2.2
 555          An app context is pushed when running ``app.cli`` commands, so
 556          ``@with_appcontext`` is no longer required for those commands.
 557  
 558      .. versionchanged:: 1.0
 559          If installed, python-dotenv will be used to load environment variables
 560          from :file:`.env` and :file:`.flaskenv` files.
 561      """
 562  
 563      def __init__(
 564          self,
 565          add_default_commands: bool = True,
 566          create_app: t.Callable[..., Flask] | None = None,
 567          add_version_option: bool = True,
 568          load_dotenv: bool = True,
 569          set_debug_flag: bool = True,
 570          **extra: t.Any,
 571      ) -> None:
 572          params: list[click.Parameter] = list(extra.pop("params", None) or ())
 573          # Processing is done with option callbacks instead of a group
 574          # callback. This allows users to make a custom group callback
 575          # without losing the behavior. --env-file must come first so
 576          # that it is eagerly evaluated before --app.
 577          params.extend((_env_file_option, _app_option, _debug_option))
 578  
 579          if add_version_option:
 580              params.append(version_option)
 581  
 582          if "context_settings" not in extra:
 583              extra["context_settings"] = {}
 584  
 585          extra["context_settings"].setdefault("auto_envvar_prefix", "FLASK")
 586  
 587          super().__init__(params=params, **extra)
 588  
 589          self.create_app = create_app
 590          self.load_dotenv = load_dotenv
 591          self.set_debug_flag = set_debug_flag
 592  
 593          if add_default_commands:
 594              self.add_command(run_command)
 595              self.add_command(shell_command)
 596              self.add_command(routes_command)
 597  
 598          self._loaded_plugin_commands = False
 599  
 600      def _load_plugin_commands(self) -> None:
 601          if self._loaded_plugin_commands:
 602              return
 603  
 604          if sys.version_info >= (3, 10):
 605              from importlib import metadata
 606          else:
 607              # Use a backport on Python < 3.10. We technically have
 608              # importlib.metadata on 3.8+, but the API changed in 3.10,
 609              # so use the backport for consistency.
 610              import importlib_metadata as metadata  # pyright: ignore
 611  
 612          for ep in metadata.entry_points(group="flask.commands"):
 613              self.add_command(ep.load(), ep.name)
 614  
 615          self._loaded_plugin_commands = True
 616  
 617      def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
 618          self._load_plugin_commands()
 619          # Look up built-in and plugin commands, which should be
 620          # available even if the app fails to load.
 621          rv = super().get_command(ctx, name)
 622  
 623          if rv is not None:
 624              return rv
 625  
 626          info = ctx.ensure_object(ScriptInfo)
 627  
 628          # Look up commands provided by the app, showing an error and
 629          # continuing if the app couldn't be loaded.
 630          try:
 631              app = info.load_app()
 632          except NoAppException as e:
 633              click.secho(f"Error: {e.format_message()}\n", err=True, fg="red")
 634              return None
 635  
 636          # Push an app context for the loaded app unless it is already
 637          # active somehow. This makes the context available to parameter
 638          # and command callbacks without needing @with_appcontext.
 639          if not current_app or current_app._get_current_object() is not app:  # type: ignore[attr-defined]
 640              ctx.with_resource(app.app_context())
 641  
 642          return app.cli.get_command(ctx, name)
 643  
 644      def list_commands(self, ctx: click.Context) -> list[str]:
 645          self._load_plugin_commands()
 646          # Start with the built-in and plugin commands.
 647          rv = set(super().list_commands(ctx))
 648          info = ctx.ensure_object(ScriptInfo)
 649  
 650          # Add commands provided by the app, showing an error and
 651          # continuing if the app couldn't be loaded.
 652          try:
 653              rv.update(info.load_app().cli.list_commands(ctx))
 654          except NoAppException as e:
 655              # When an app couldn't be loaded, show the error message
 656              # without the traceback.
 657              click.secho(f"Error: {e.format_message()}\n", err=True, fg="red")
 658          except Exception:
 659              # When any other errors occurred during loading, show the
 660              # full traceback.
 661              click.secho(f"{traceback.format_exc()}\n", err=True, fg="red")
 662  
 663          return sorted(rv)
 664  
 665      def make_context(
 666          self,
 667          info_name: str | None,
 668          args: list[str],
 669          parent: click.Context | None = None,
 670          **extra: t.Any,
 671      ) -> click.Context:
 672          # Set a flag to tell app.run to become a no-op. If app.run was
 673          # not in a __name__ == __main__ guard, it would start the server
 674          # when importing, blocking whatever command is being called.
 675          os.environ["FLASK_RUN_FROM_CLI"] = "true"
 676  
 677          if "obj" not in extra and "obj" not in self.context_settings:
 678              extra["obj"] = ScriptInfo(
 679                  create_app=self.create_app,
 680                  set_debug_flag=self.set_debug_flag,
 681                  load_dotenv_defaults=self.load_dotenv,
 682              )
 683  
 684          return super().make_context(info_name, args, parent=parent, **extra)
 685  
 686      def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
 687          if not args and self.no_args_is_help:
 688              # Attempt to load --env-file and --app early in case they
 689              # were given as env vars. Otherwise no_args_is_help will not
 690              # see commands from app.cli.
 691              _env_file_option.handle_parse_result(ctx, {}, [])
 692              _app_option.handle_parse_result(ctx, {}, [])
 693  
 694          return super().parse_args(ctx, args)
 695  
 696  
 697  def _path_is_ancestor(path: str, other: str) -> bool:
 698      """Take ``other`` and remove the length of ``path`` from it. Then join it
 699      to ``path``. If it is the original value, ``path`` is an ancestor of
 700      ``other``."""
 701      return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other
 702  
 703  
 704  def load_dotenv(
 705      path: str | os.PathLike[str] | None = None, load_defaults: bool = True
 706  ) -> bool:
 707      """Load "dotenv" files to set environment variables. A given path takes
 708      precedence over ``.env``, which takes precedence over ``.flaskenv``. After
 709      loading and combining these files, values are only set if the key is not
 710      already set in ``os.environ``.
 711  
 712      This is a no-op if `python-dotenv`_ is not installed.
 713  
 714      .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme
 715  
 716      :param path: Load the file at this location.
 717      :param load_defaults: Search for and load the default ``.flaskenv`` and
 718          ``.env`` files.
 719      :return: ``True`` if at least one env var was loaded.
 720  
 721      .. versionchanged:: 3.1
 722          Added the ``load_defaults`` parameter. A given path takes precedence
 723          over default files.
 724  
 725      .. versionchanged:: 2.0
 726          The current directory is not changed to the location of the
 727          loaded file.
 728  
 729      .. versionchanged:: 2.0
 730          When loading the env files, set the default encoding to UTF-8.
 731  
 732      .. versionchanged:: 1.1.0
 733          Returns ``False`` when python-dotenv is not installed, or when
 734          the given path isn't a file.
 735  
 736      .. versionadded:: 1.0
 737      """
 738      try:
 739          import dotenv
 740      except ImportError:
 741          if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"):
 742              click.secho(
 743                  " * Tip: There are .env files present. Install python-dotenv"
 744                  " to use them.",
 745                  fg="yellow",
 746                  err=True,
 747              )
 748  
 749          return False
 750  
 751      data: dict[str, str | None] = {}
 752  
 753      if load_defaults:
 754          for default_name in (".flaskenv", ".env"):
 755              if not (default_path := dotenv.find_dotenv(default_name, usecwd=True)):
 756                  continue
 757  
 758              data |= dotenv.dotenv_values(default_path, encoding="utf-8")
 759  
 760      if path is not None and os.path.isfile(path):
 761          data |= dotenv.dotenv_values(path, encoding="utf-8")
 762  
 763      for key, value in data.items():
 764          if key in os.environ or value is None:
 765              continue
 766  
 767          os.environ[key] = value
 768  
 769      return bool(data)  # True if at least one env var was loaded.
 770  
 771  
 772  def show_server_banner(debug: bool, app_import_path: str | None) -> None:
 773      """Show extra startup messages the first time the server is run,
 774      ignoring the reloader.
 775      """
 776      if is_running_from_reloader():
 777          return
 778  
 779      if app_import_path is not None:
 780          click.echo(f" * Serving Flask app '{app_import_path}'")
 781  
 782      if debug is not None:
 783          click.echo(f" * Debug mode: {'on' if debug else 'off'}")
 784  
 785  
 786  class CertParamType(click.ParamType):
 787      """Click option type for the ``--cert`` option. Allows either an
 788      existing file, the string ``'adhoc'``, or an import for a
 789      :class:`~ssl.SSLContext` object.
 790      """
 791  
 792      name = "path"
 793  
 794      def __init__(self) -> None:
 795          self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True)
 796  
 797      def convert(
 798          self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None
 799      ) -> t.Any:
 800          try:
 801              import ssl
 802          except ImportError:
 803              raise click.BadParameter(
 804                  'Using "--cert" requires Python to be compiled with SSL support.',
 805                  ctx,
 806                  param,
 807              ) from None
 808  
 809          try:
 810              return self.path_type(value, param, ctx)
 811          except click.BadParameter:
 812              value = click.STRING(value, param, ctx).lower()
 813  
 814              if value == "adhoc":
 815                  try:
 816                      import cryptography  # noqa: F401
 817                  except ImportError:
 818                      raise click.BadParameter(
 819                          "Using ad-hoc certificates requires the cryptography library.",
 820                          ctx,
 821                          param,
 822                      ) from None
 823  
 824                  return value
 825  
 826              obj = import_string(value, silent=True)
 827  
 828              if isinstance(obj, ssl.SSLContext):
 829                  return obj
 830  
 831              raise
 832  
 833  
 834  def _validate_key(ctx: click.Context, param: click.Parameter, value: t.Any) -> t.Any:
 835      """The ``--key`` option must be specified when ``--cert`` is a file.
 836      Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed.
 837      """
 838      cert = ctx.params.get("cert")
 839      is_adhoc = cert == "adhoc"
 840  
 841      try:
 842          import ssl
 843      except ImportError:
 844          is_context = False
 845      else:
 846          is_context = isinstance(cert, ssl.SSLContext)
 847  
 848      if value is not None:
 849          if is_adhoc:
 850              raise click.BadParameter(
 851                  'When "--cert" is "adhoc", "--key" is not used.', ctx, param
 852              )
 853  
 854          if is_context:
 855              raise click.BadParameter(
 856                  'When "--cert" is an SSLContext object, "--key" is not used.',
 857                  ctx,
 858                  param,
 859              )
 860  
 861          if not cert:
 862              raise click.BadParameter('"--cert" must also be specified.', ctx, param)
 863  
 864          ctx.params["cert"] = cert, value
 865  
 866      else:
 867          if cert and not (is_adhoc or is_context):
 868              raise click.BadParameter('Required when using "--cert".', ctx, param)
 869  
 870      return value
 871  
 872  
 873  class SeparatedPathType(click.Path):
 874      """Click option type that accepts a list of values separated by the
 875      OS's path separator (``:``, ``;`` on Windows). Each value is
 876      validated as a :class:`click.Path` type.
 877      """
 878  
 879      def convert(
 880          self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None
 881      ) -> t.Any:
 882          items = self.split_envvar_value(value)
 883          # can't call no-arg super() inside list comprehension until Python 3.12
 884          super_convert = super().convert
 885          return [super_convert(item, param, ctx) for item in items]
 886  
 887  
 888  @click.command("run", short_help="Run a development server.")
 889  @click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.")
 890  @click.option("--port", "-p", default=5000, help="The port to bind to.")
 891  @click.option(
 892      "--cert",
 893      type=CertParamType(),
 894      help="Specify a certificate file to use HTTPS.",
 895      is_eager=True,
 896  )
 897  @click.option(
 898      "--key",
 899      type=click.Path(exists=True, dir_okay=False, resolve_path=True),
 900      callback=_validate_key,
 901      expose_value=False,
 902      help="The key file to use when specifying a certificate.",
 903  )
 904  @click.option(
 905      "--reload/--no-reload",
 906      default=None,
 907      help="Enable or disable the reloader. By default the reloader "
 908      "is active if debug is enabled.",
 909  )
 910  @click.option(
 911      "--debugger/--no-debugger",
 912      default=None,
 913      help="Enable or disable the debugger. By default the debugger "
 914      "is active if debug is enabled.",
 915  )
 916  @click.option(
 917      "--with-threads/--without-threads",
 918      default=True,
 919      help="Enable or disable multithreading.",
 920  )
 921  @click.option(
 922      "--extra-files",
 923      default=None,
 924      type=SeparatedPathType(),
 925      help=(
 926          "Extra files that trigger a reload on change. Multiple paths"
 927          f" are separated by {os.path.pathsep!r}."
 928      ),
 929  )
 930  @click.option(
 931      "--exclude-patterns",
 932      default=None,
 933      type=SeparatedPathType(),
 934      help=(
 935          "Files matching these fnmatch patterns will not trigger a reload"
 936          " on change. Multiple patterns are separated by"
 937          f" {os.path.pathsep!r}."
 938      ),
 939  )
 940  @pass_script_info
 941  def run_command(
 942      info: ScriptInfo,
 943      host: str,
 944      port: int,
 945      reload: bool,
 946      debugger: bool,
 947      with_threads: bool,
 948      cert: ssl.SSLContext | tuple[str, str | None] | t.Literal["adhoc"] | None,
 949      extra_files: list[str] | None,
 950      exclude_patterns: list[str] | None,
 951  ) -> None:
 952      """Run a local development server.
 953  
 954      This server is for development purposes only. It does not provide
 955      the stability, security, or performance of production WSGI servers.
 956  
 957      The reloader and debugger are enabled by default with the '--debug'
 958      option.
 959      """
 960      try:
 961          app: WSGIApplication = info.load_app()  # pyright: ignore
 962      except Exception as e:
 963          if is_running_from_reloader():
 964              # When reloading, print out the error immediately, but raise
 965              # it later so the debugger or server can handle it.
 966              traceback.print_exc()
 967              err = e
 968  
 969              def app(
 970                  environ: WSGIEnvironment, start_response: StartResponse
 971              ) -> cabc.Iterable[bytes]:
 972                  raise err from None
 973  
 974          else:
 975              # When not reloading, raise the error immediately so the
 976              # command fails.
 977              raise e from None
 978  
 979      debug = get_debug_flag()
 980  
 981      if reload is None:
 982          reload = debug
 983  
 984      if debugger is None:
 985          debugger = debug
 986  
 987      show_server_banner(debug, info.app_import_path)
 988  
 989      run_simple(
 990          host,
 991          port,
 992          app,
 993          use_reloader=reload,
 994          use_debugger=debugger,
 995          threaded=with_threads,
 996          ssl_context=cert,
 997          extra_files=extra_files,
 998          exclude_patterns=exclude_patterns,
 999      )
1000  
1001  
1002  run_command.params.insert(0, _debug_option)
1003  
1004  
1005  @click.command("shell", short_help="Run a shell in the app context.")
1006  @with_appcontext
1007  def shell_command() -> None:
1008      """Run an interactive Python shell in the context of a given
1009      Flask application.  The application will populate the default
1010      namespace of this shell according to its configuration.
1011  
1012      This is useful for executing small snippets of management code
1013      without having to manually configure the application.
1014      """
1015      import code
1016  
1017      banner = (
1018          f"Python {sys.version} on {sys.platform}\n"
1019          f"App: {current_app.import_name}\n"
1020          f"Instance: {current_app.instance_path}"
1021      )
1022      ctx: dict[str, t.Any] = {}
1023  
1024      # Support the regular Python interpreter startup script if someone
1025      # is using it.
1026      startup = os.environ.get("PYTHONSTARTUP")
1027      if startup and os.path.isfile(startup):
1028          with open(startup) as f:
1029              eval(compile(f.read(), startup, "exec"), ctx)
1030  
1031      ctx.update(current_app.make_shell_context())
1032  
1033      # Site, customize, or startup script can set a hook to call when
1034      # entering interactive mode. The default one sets up readline with
1035      # tab and history completion.
1036      interactive_hook = getattr(sys, "__interactivehook__", None)
1037  
1038      if interactive_hook is not None:
1039          try:
1040              import readline
1041              from rlcompleter import Completer
1042          except ImportError:
1043              pass
1044          else:
1045              # rlcompleter uses __main__.__dict__ by default, which is
1046              # flask.__main__. Use the shell context instead.
1047              readline.set_completer(Completer(ctx).complete)
1048  
1049          interactive_hook()
1050  
1051      code.interact(banner=banner, local=ctx)
1052  
1053  
1054  @click.command("routes", short_help="Show the routes for the app.")
1055  @click.option(
1056      "--sort",
1057      "-s",
1058      type=click.Choice(("endpoint", "methods", "domain", "rule", "match")),
1059      default="endpoint",
1060      help=(
1061          "Method to sort routes by. 'match' is the order that Flask will match routes"
1062          " when dispatching a request."
1063      ),
1064  )
1065  @click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.")
1066  @with_appcontext
1067  def routes_command(sort: str, all_methods: bool) -> None:
1068      """Show all registered routes with endpoints and methods."""
1069      rules = list(current_app.url_map.iter_rules())
1070  
1071      if not rules:
1072          click.echo("No routes were registered.")
1073          return
1074  
1075      ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"}
1076      host_matching = current_app.url_map.host_matching
1077      has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules)
1078      rows = []
1079  
1080      for rule in rules:
1081          row = [
1082              rule.endpoint,
1083              ", ".join(sorted((rule.methods or set()) - ignored_methods)),
1084          ]
1085  
1086          if has_domain:
1087              row.append((rule.host if host_matching else rule.subdomain) or "")
1088  
1089          row.append(rule.rule)
1090          rows.append(row)
1091  
1092      headers = ["Endpoint", "Methods"]
1093      sorts = ["endpoint", "methods"]
1094  
1095      if has_domain:
1096          headers.append("Host" if host_matching else "Subdomain")
1097          sorts.append("domain")
1098  
1099      headers.append("Rule")
1100      sorts.append("rule")
1101  
1102      try:
1103          rows.sort(key=itemgetter(sorts.index(sort)))
1104      except ValueError:
1105          pass
1106  
1107      rows.insert(0, headers)
1108      widths = [max(len(row[i]) for row in rows) for i in range(len(headers))]
1109      rows.insert(1, ["-" * w for w in widths])
1110      template = "  ".join(f"{{{i}:<{w}}}" for i, w in enumerate(widths))
1111  
1112      for row in rows:
1113          click.echo(template.format(*row))
1114  
1115  
1116  cli = FlaskGroup(
1117      name="flask",
1118      help="""\
1119  A general utility script for Flask applications.
1120  
1121  An application to load must be given with the '--app' option,
1122  'FLASK_APP' environment variable, or with a 'wsgi.py' or 'app.py' file
1123  in the current directory.
1124  """,
1125  )
1126  
1127  
1128  def main() -> None:
1129      cli.main()
1130  
1131  
1132  if __name__ == "__main__":
1133      main()
1134