Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/flask/cli.py: 24%

428 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-09 07:17 +0000

1from __future__ import annotations 

2 

3import ast 

4import importlib.metadata 

5import inspect 

6import os 

7import platform 

8import re 

9import sys 

10import traceback 

11import typing as t 

12from functools import update_wrapper 

13from operator import itemgetter 

14 

15import click 

16from click.core import ParameterSource 

17from werkzeug import run_simple 

18from werkzeug.serving import is_running_from_reloader 

19from werkzeug.utils import import_string 

20 

21from .globals import current_app 

22from .helpers import get_debug_flag 

23from .helpers import get_load_dotenv 

24 

25if t.TYPE_CHECKING: 

26 from .app import Flask 

27 

28 

29class NoAppException(click.UsageError): 

30 """Raised if an application cannot be found or loaded.""" 

31 

32 

33def 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 

86def _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 

112def 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 

188def 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 

217def 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 

241def 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 

257version_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 

267class 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 

335pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) 

336 

337 

338def 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 

363class 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 

394def _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 

422def _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 

447def _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 

482class 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 

648def _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 

655def 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 

716def 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 

730class 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 

776def _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 

813class 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 

878def 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 

937run_command.params.insert(0, _debug_option) 

938 

939 

940@click.command("shell", short_help="Run a shell in the app context.") 

941@with_appcontext 

942def 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 

1002def 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 

1051cli = FlaskGroup( 

1052 name="flask", 

1053 help="""\ 

1054A general utility script for Flask applications. 

1055 

1056An application to load must be given with the '--app' option, 

1057'FLASK_APP' environment variable, or with a 'wsgi.py' or 'app.py' file 

1058in the current directory. 

1059""", 

1060) 

1061 

1062 

1063def main() -> None: 

1064 cli.main() 

1065 

1066 

1067if __name__ == "__main__": 

1068 main()