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