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