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

414 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +0000

1from __future__ import annotations 

2 

3import ast 

4import inspect 

5import os 

6import platform 

7import re 

8import sys 

9import traceback 

10import typing as t 

11from functools import update_wrapper 

12from operator import attrgetter 

13 

14import click 

15from click.core import ParameterSource 

16from werkzeug import run_simple 

17from werkzeug.serving import is_running_from_reloader 

18from werkzeug.utils import import_string 

19 

20from .globals import current_app 

21from .helpers import get_debug_flag 

22from .helpers import get_load_dotenv 

23 

24if t.TYPE_CHECKING: 

25 from .app import Flask 

26 

27 

28class NoAppException(click.UsageError): 

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

30 

31 

32def find_best_app(module): 

33 """Given a module instance this tries to find the best possible 

34 application in the module or raises an exception. 

35 """ 

36 from . import Flask 

37 

38 # Search for the most common names first. 

39 for attr_name in ("app", "application"): 

40 app = getattr(module, attr_name, None) 

41 

42 if isinstance(app, Flask): 

43 return app 

44 

45 # Otherwise find the only object that is a Flask instance. 

46 matches = [v for v in module.__dict__.values() if isinstance(v, Flask)] 

47 

48 if len(matches) == 1: 

49 return matches[0] 

50 elif len(matches) > 1: 

51 raise NoAppException( 

52 "Detected multiple Flask applications in module" 

53 f" '{module.__name__}'. Use '{module.__name__}:name'" 

54 " to specify the correct one." 

55 ) 

56 

57 # Search for app factory functions. 

58 for attr_name in ("create_app", "make_app"): 

59 app_factory = getattr(module, attr_name, None) 

60 

61 if inspect.isfunction(app_factory): 

62 try: 

63 app = app_factory() 

64 

65 if isinstance(app, Flask): 

66 return app 

67 except TypeError as e: 

68 if not _called_with_wrong_args(app_factory): 

69 raise 

70 

71 raise NoAppException( 

72 f"Detected factory '{attr_name}' in module '{module.__name__}'," 

73 " but could not call it without arguments. Use" 

74 f" '{module.__name__}:{attr_name}(args)'" 

75 " to specify arguments." 

76 ) from e 

77 

78 raise NoAppException( 

79 "Failed to find Flask application or factory in module" 

80 f" '{module.__name__}'. Use '{module.__name__}:name'" 

81 " to specify one." 

82 ) 

83 

84 

85def _called_with_wrong_args(f): 

86 """Check whether calling a function raised a ``TypeError`` because 

87 the call failed or because something in the factory raised the 

88 error. 

89 

90 :param f: The function that was called. 

91 :return: ``True`` if the call failed. 

92 """ 

93 tb = sys.exc_info()[2] 

94 

95 try: 

96 while tb is not None: 

97 if tb.tb_frame.f_code is f.__code__: 

98 # In the function, it was called successfully. 

99 return False 

100 

101 tb = tb.tb_next 

102 

103 # Didn't reach the function. 

104 return True 

105 finally: 

106 # Delete tb to break a circular reference. 

107 # https://docs.python.org/2/library/sys.html#sys.exc_info 

108 del tb 

109 

110 

111def find_app_by_string(module, app_name): 

112 """Check if the given string is a variable name or a function. Call 

113 a function to get the app instance, or return the variable directly. 

114 """ 

115 from . import Flask 

116 

117 # Parse app_name as a single expression to determine if it's a valid 

118 # attribute name or function call. 

119 try: 

120 expr = ast.parse(app_name.strip(), mode="eval").body 

121 except SyntaxError: 

122 raise NoAppException( 

123 f"Failed to parse {app_name!r} as an attribute name or function call." 

124 ) from None 

125 

126 if isinstance(expr, ast.Name): 

127 name = expr.id 

128 args = [] 

129 kwargs = {} 

130 elif isinstance(expr, ast.Call): 

131 # Ensure the function name is an attribute name only. 

132 if not isinstance(expr.func, ast.Name): 

133 raise NoAppException( 

134 f"Function reference must be a simple name: {app_name!r}." 

135 ) 

136 

137 name = expr.func.id 

138 

139 # Parse the positional and keyword arguments as literals. 

140 try: 

141 args = [ast.literal_eval(arg) for arg in expr.args] 

142 kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expr.keywords} 

143 except ValueError: 

144 # literal_eval gives cryptic error messages, show a generic 

145 # message with the full expression instead. 

146 raise NoAppException( 

147 f"Failed to parse arguments as literal values: {app_name!r}." 

148 ) from None 

149 else: 

150 raise NoAppException( 

151 f"Failed to parse {app_name!r} as an attribute name or function call." 

152 ) 

153 

154 try: 

155 attr = getattr(module, name) 

156 except AttributeError as e: 

157 raise NoAppException( 

158 f"Failed to find attribute {name!r} in {module.__name__!r}." 

159 ) from e 

160 

161 # If the attribute is a function, call it with any args and kwargs 

162 # to get the real application. 

163 if inspect.isfunction(attr): 

164 try: 

165 app = attr(*args, **kwargs) 

166 except TypeError as e: 

167 if not _called_with_wrong_args(attr): 

168 raise 

169 

170 raise NoAppException( 

171 f"The factory {app_name!r} in module" 

172 f" {module.__name__!r} could not be called with the" 

173 " specified arguments." 

174 ) from e 

175 else: 

176 app = attr 

177 

178 if isinstance(app, Flask): 

179 return app 

180 

181 raise NoAppException( 

182 "A valid Flask application was not obtained from" 

183 f" '{module.__name__}:{app_name}'." 

184 ) 

185 

186 

187def prepare_import(path): 

188 """Given a filename this will try to calculate the python path, add it 

189 to the search path and return the actual module name that is expected. 

190 """ 

191 path = os.path.realpath(path) 

192 

193 fname, ext = os.path.splitext(path) 

194 if ext == ".py": 

195 path = fname 

196 

197 if os.path.basename(path) == "__init__": 

198 path = os.path.dirname(path) 

199 

200 module_name = [] 

201 

202 # move up until outside package structure (no __init__.py) 

203 while True: 

204 path, name = os.path.split(path) 

205 module_name.append(name) 

206 

207 if not os.path.exists(os.path.join(path, "__init__.py")): 

208 break 

209 

210 if sys.path[0] != path: 

211 sys.path.insert(0, path) 

212 

213 return ".".join(module_name[::-1]) 

214 

215 

216def locate_app(module_name, app_name, raise_if_not_found=True): 

217 try: 

218 __import__(module_name) 

219 except ImportError: 

220 # Reraise the ImportError if it occurred within the imported module. 

221 # Determine this by checking whether the trace has a depth > 1. 

222 if sys.exc_info()[2].tb_next: 

223 raise NoAppException( 

224 f"While importing {module_name!r}, an ImportError was" 

225 f" raised:\n\n{traceback.format_exc()}" 

226 ) from None 

227 elif raise_if_not_found: 

228 raise NoAppException(f"Could not import {module_name!r}.") from None 

229 else: 

230 return 

231 

232 module = sys.modules[module_name] 

233 

234 if app_name is None: 

235 return find_best_app(module) 

236 else: 

237 return find_app_by_string(module, app_name) 

238 

239 

240def get_version(ctx, param, value): 

241 if not value or ctx.resilient_parsing: 

242 return 

243 

244 import werkzeug 

245 from . import __version__ 

246 

247 click.echo( 

248 f"Python {platform.python_version()}\n" 

249 f"Flask {__version__}\n" 

250 f"Werkzeug {werkzeug.__version__}", 

251 color=ctx.color, 

252 ) 

253 ctx.exit() 

254 

255 

256version_option = click.Option( 

257 ["--version"], 

258 help="Show the Flask version.", 

259 expose_value=False, 

260 callback=get_version, 

261 is_flag=True, 

262 is_eager=True, 

263) 

264 

265 

266class ScriptInfo: 

267 """Helper object to deal with Flask applications. This is usually not 

268 necessary to interface with as it's used internally in the dispatching 

269 to click. In future versions of Flask this object will most likely play 

270 a bigger role. Typically it's created automatically by the 

271 :class:`FlaskGroup` but you can also manually create it and pass it 

272 onwards as click object. 

273 """ 

274 

275 def __init__( 

276 self, 

277 app_import_path: str | None = None, 

278 create_app: t.Callable[..., Flask] | None = None, 

279 set_debug_flag: bool = True, 

280 ) -> None: 

281 #: Optionally the import path for the Flask application. 

282 self.app_import_path = app_import_path 

283 #: Optionally a function that is passed the script info to create 

284 #: the instance of the application. 

285 self.create_app = create_app 

286 #: A dictionary with arbitrary data that can be associated with 

287 #: this script info. 

288 self.data: t.Dict[t.Any, t.Any] = {} 

289 self.set_debug_flag = set_debug_flag 

290 self._loaded_app: Flask | None = None 

291 

292 def load_app(self) -> Flask: 

293 """Loads the Flask app (if not yet loaded) and returns it. Calling 

294 this multiple times will just result in the already loaded app to 

295 be returned. 

296 """ 

297 if self._loaded_app is not None: 

298 return self._loaded_app 

299 

300 if self.create_app is not None: 

301 app = self.create_app() 

302 else: 

303 if self.app_import_path: 

304 path, name = ( 

305 re.split(r":(?![\\/])", self.app_import_path, 1) + [None] 

306 )[:2] 

307 import_name = prepare_import(path) 

308 app = locate_app(import_name, name) 

309 else: 

310 for path in ("wsgi.py", "app.py"): 

311 import_name = prepare_import(path) 

312 app = locate_app(import_name, None, raise_if_not_found=False) 

313 

314 if app: 

315 break 

316 

317 if not app: 

318 raise NoAppException( 

319 "Could not locate a Flask application. Use the" 

320 " 'flask --app' option, 'FLASK_APP' environment" 

321 " variable, or a 'wsgi.py' or 'app.py' file in the" 

322 " current directory." 

323 ) 

324 

325 if self.set_debug_flag: 

326 # Update the app's debug flag through the descriptor so that 

327 # other values repopulate as well. 

328 app.debug = get_debug_flag() 

329 

330 self._loaded_app = app 

331 return app 

332 

333 

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

335 

336 

337def with_appcontext(f): 

338 """Wraps a callback so that it's guaranteed to be executed with the 

339 script's application context. 

340 

341 Custom commands (and their options) registered under ``app.cli`` or 

342 ``blueprint.cli`` will always have an app context available, this 

343 decorator is not required in that case. 

344 

345 .. versionchanged:: 2.2 

346 The app context is active for subcommands as well as the 

347 decorated callback. The app context is always available to 

348 ``app.cli`` command and parameter callbacks. 

349 """ 

350 

351 @click.pass_context 

352 def decorator(__ctx, *args, **kwargs): 

353 if not current_app: 

354 app = __ctx.ensure_object(ScriptInfo).load_app() 

355 __ctx.with_resource(app.app_context()) 

356 

357 return __ctx.invoke(f, *args, **kwargs) 

358 

359 return update_wrapper(decorator, f) 

360 

361 

362class AppGroup(click.Group): 

363 """This works similar to a regular click :class:`~click.Group` but it 

364 changes the behavior of the :meth:`command` decorator so that it 

365 automatically wraps the functions in :func:`with_appcontext`. 

366 

367 Not to be confused with :class:`FlaskGroup`. 

368 """ 

369 

370 def command(self, *args, **kwargs): 

371 """This works exactly like the method of the same name on a regular 

372 :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` 

373 unless it's disabled by passing ``with_appcontext=False``. 

374 """ 

375 wrap_for_ctx = kwargs.pop("with_appcontext", True) 

376 

377 def decorator(f): 

378 if wrap_for_ctx: 

379 f = with_appcontext(f) 

380 return click.Group.command(self, *args, **kwargs)(f) 

381 

382 return decorator 

383 

384 def group(self, *args, **kwargs): 

385 """This works exactly like the method of the same name on a regular 

386 :class:`click.Group` but it defaults the group class to 

387 :class:`AppGroup`. 

388 """ 

389 kwargs.setdefault("cls", AppGroup) 

390 return click.Group.group(self, *args, **kwargs) 

391 

392 

393def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None: 

394 if value is None: 

395 return None 

396 

397 info = ctx.ensure_object(ScriptInfo) 

398 info.app_import_path = value 

399 return value 

400 

401 

402# This option is eager so the app will be available if --help is given. 

403# --help is also eager, so --app must be before it in the param list. 

404# no_args_is_help bypasses eager processing, so this option must be 

405# processed manually in that case to ensure FLASK_APP gets picked up. 

406_app_option = click.Option( 

407 ["-A", "--app"], 

408 metavar="IMPORT", 

409 help=( 

410 "The Flask application or factory function to load, in the form 'module:name'." 

411 " Module can be a dotted import or file path. Name is not required if it is" 

412 " 'app', 'application', 'create_app', or 'make_app', and can be 'name(args)' to" 

413 " pass arguments." 

414 ), 

415 is_eager=True, 

416 expose_value=False, 

417 callback=_set_app, 

418) 

419 

420 

421def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None: 

422 # If the flag isn't provided, it will default to False. Don't use 

423 # that, let debug be set by env in that case. 

424 source = ctx.get_parameter_source(param.name) # type: ignore[arg-type] 

425 

426 if source is not None and source in ( 

427 ParameterSource.DEFAULT, 

428 ParameterSource.DEFAULT_MAP, 

429 ): 

430 return None 

431 

432 # Set with env var instead of ScriptInfo.load so that it can be 

433 # accessed early during a factory function. 

434 os.environ["FLASK_DEBUG"] = "1" if value else "0" 

435 return value 

436 

437 

438_debug_option = click.Option( 

439 ["--debug/--no-debug"], 

440 help="Set debug mode.", 

441 expose_value=False, 

442 callback=_set_debug, 

443) 

444 

445 

446def _env_file_callback( 

447 ctx: click.Context, param: click.Option, value: str | None 

448) -> str | None: 

449 if value is None: 

450 return None 

451 

452 import importlib 

453 

454 try: 

455 importlib.import_module("dotenv") 

456 except ImportError: 

457 raise click.BadParameter( 

458 "python-dotenv must be installed to load an env file.", 

459 ctx=ctx, 

460 param=param, 

461 ) from None 

462 

463 # Don't check FLASK_SKIP_DOTENV, that only disables automatically 

464 # loading .env and .flaskenv files. 

465 load_dotenv(value) 

466 return value 

467 

468 

469# This option is eager so env vars are loaded as early as possible to be 

470# used by other options. 

471_env_file_option = click.Option( 

472 ["-e", "--env-file"], 

473 type=click.Path(exists=True, dir_okay=False), 

474 help="Load environment variables from this file. python-dotenv must be installed.", 

475 is_eager=True, 

476 expose_value=False, 

477 callback=_env_file_callback, 

478) 

479 

480 

481class FlaskGroup(AppGroup): 

482 """Special subclass of the :class:`AppGroup` group that supports 

483 loading more commands from the configured Flask app. Normally a 

484 developer does not have to interface with this class but there are 

485 some very advanced use cases for which it makes sense to create an 

486 instance of this. see :ref:`custom-scripts`. 

487 

488 :param add_default_commands: if this is True then the default run and 

489 shell commands will be added. 

490 :param add_version_option: adds the ``--version`` option. 

491 :param create_app: an optional callback that is passed the script info and 

492 returns the loaded app. 

493 :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` 

494 files to set environment variables. Will also change the working 

495 directory to the directory containing the first file found. 

496 :param set_debug_flag: Set the app's debug flag. 

497 

498 .. versionchanged:: 2.2 

499 Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options. 

500 

501 .. versionchanged:: 2.2 

502 An app context is pushed when running ``app.cli`` commands, so 

503 ``@with_appcontext`` is no longer required for those commands. 

504 

505 .. versionchanged:: 1.0 

506 If installed, python-dotenv will be used to load environment variables 

507 from :file:`.env` and :file:`.flaskenv` files. 

508 """ 

509 

510 def __init__( 

511 self, 

512 add_default_commands: bool = True, 

513 create_app: t.Callable[..., Flask] | None = None, 

514 add_version_option: bool = True, 

515 load_dotenv: bool = True, 

516 set_debug_flag: bool = True, 

517 **extra: t.Any, 

518 ) -> None: 

519 params = list(extra.pop("params", None) or ()) 

520 # Processing is done with option callbacks instead of a group 

521 # callback. This allows users to make a custom group callback 

522 # without losing the behavior. --env-file must come first so 

523 # that it is eagerly evaluated before --app. 

524 params.extend((_env_file_option, _app_option, _debug_option)) 

525 

526 if add_version_option: 

527 params.append(version_option) 

528 

529 if "context_settings" not in extra: 

530 extra["context_settings"] = {} 

531 

532 extra["context_settings"].setdefault("auto_envvar_prefix", "FLASK") 

533 

534 super().__init__(params=params, **extra) 

535 

536 self.create_app = create_app 

537 self.load_dotenv = load_dotenv 

538 self.set_debug_flag = set_debug_flag 

539 

540 if add_default_commands: 

541 self.add_command(run_command) 

542 self.add_command(shell_command) 

543 self.add_command(routes_command) 

544 

545 self._loaded_plugin_commands = False 

546 

547 def _load_plugin_commands(self): 

548 if self._loaded_plugin_commands: 

549 return 

550 

551 if sys.version_info >= (3, 10): 

552 from importlib import metadata 

553 else: 

554 # Use a backport on Python < 3.10. We technically have 

555 # importlib.metadata on 3.8+, but the API changed in 3.10, 

556 # so use the backport for consistency. 

557 import importlib_metadata as metadata 

558 

559 for ep in metadata.entry_points(group="flask.commands"): 

560 self.add_command(ep.load(), ep.name) 

561 

562 self._loaded_plugin_commands = True 

563 

564 def get_command(self, ctx, name): 

565 self._load_plugin_commands() 

566 # Look up built-in and plugin commands, which should be 

567 # available even if the app fails to load. 

568 rv = super().get_command(ctx, name) 

569 

570 if rv is not None: 

571 return rv 

572 

573 info = ctx.ensure_object(ScriptInfo) 

574 

575 # Look up commands provided by the app, showing an error and 

576 # continuing if the app couldn't be loaded. 

577 try: 

578 app = info.load_app() 

579 except NoAppException as e: 

580 click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") 

581 return None 

582 

583 # Push an app context for the loaded app unless it is already 

584 # active somehow. This makes the context available to parameter 

585 # and command callbacks without needing @with_appcontext. 

586 if not current_app or current_app._get_current_object() is not app: 

587 ctx.with_resource(app.app_context()) 

588 

589 return app.cli.get_command(ctx, name) 

590 

591 def list_commands(self, ctx): 

592 self._load_plugin_commands() 

593 # Start with the built-in and plugin commands. 

594 rv = set(super().list_commands(ctx)) 

595 info = ctx.ensure_object(ScriptInfo) 

596 

597 # Add commands provided by the app, showing an error and 

598 # continuing if the app couldn't be loaded. 

599 try: 

600 rv.update(info.load_app().cli.list_commands(ctx)) 

601 except NoAppException as e: 

602 # When an app couldn't be loaded, show the error message 

603 # without the traceback. 

604 click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") 

605 except Exception: 

606 # When any other errors occurred during loading, show the 

607 # full traceback. 

608 click.secho(f"{traceback.format_exc()}\n", err=True, fg="red") 

609 

610 return sorted(rv) 

611 

612 def make_context( 

613 self, 

614 info_name: str | None, 

615 args: list[str], 

616 parent: click.Context | None = None, 

617 **extra: t.Any, 

618 ) -> click.Context: 

619 # Set a flag to tell app.run to become a no-op. If app.run was 

620 # not in a __name__ == __main__ guard, it would start the server 

621 # when importing, blocking whatever command is being called. 

622 os.environ["FLASK_RUN_FROM_CLI"] = "true" 

623 

624 # Attempt to load .env and .flask env files. The --env-file 

625 # option can cause another file to be loaded. 

626 if get_load_dotenv(self.load_dotenv): 

627 load_dotenv() 

628 

629 if "obj" not in extra and "obj" not in self.context_settings: 

630 extra["obj"] = ScriptInfo( 

631 create_app=self.create_app, set_debug_flag=self.set_debug_flag 

632 ) 

633 

634 return super().make_context(info_name, args, parent=parent, **extra) 

635 

636 def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: 

637 if not args and self.no_args_is_help: 

638 # Attempt to load --env-file and --app early in case they 

639 # were given as env vars. Otherwise no_args_is_help will not 

640 # see commands from app.cli. 

641 _env_file_option.handle_parse_result(ctx, {}, []) 

642 _app_option.handle_parse_result(ctx, {}, []) 

643 

644 return super().parse_args(ctx, args) 

645 

646 

647def _path_is_ancestor(path, other): 

648 """Take ``other`` and remove the length of ``path`` from it. Then join it 

649 to ``path``. If it is the original value, ``path`` is an ancestor of 

650 ``other``.""" 

651 return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other 

652 

653 

654def load_dotenv(path: str | os.PathLike | None = None) -> bool: 

655 """Load "dotenv" files in order of precedence to set environment variables. 

656 

657 If an env var is already set it is not overwritten, so earlier files in the 

658 list are preferred over later files. 

659 

660 This is a no-op if `python-dotenv`_ is not installed. 

661 

662 .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme 

663 

664 :param path: Load the file at this location instead of searching. 

665 :return: ``True`` if a file was loaded. 

666 

667 .. versionchanged:: 2.0 

668 The current directory is not changed to the location of the 

669 loaded file. 

670 

671 .. versionchanged:: 2.0 

672 When loading the env files, set the default encoding to UTF-8. 

673 

674 .. versionchanged:: 1.1.0 

675 Returns ``False`` when python-dotenv is not installed, or when 

676 the given path isn't a file. 

677 

678 .. versionadded:: 1.0 

679 """ 

680 try: 

681 import dotenv 

682 except ImportError: 

683 if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"): 

684 click.secho( 

685 " * Tip: There are .env or .flaskenv files present." 

686 ' Do "pip install python-dotenv" to use them.', 

687 fg="yellow", 

688 err=True, 

689 ) 

690 

691 return False 

692 

693 # Always return after attempting to load a given path, don't load 

694 # the default files. 

695 if path is not None: 

696 if os.path.isfile(path): 

697 return dotenv.load_dotenv(path, encoding="utf-8") 

698 

699 return False 

700 

701 loaded = False 

702 

703 for name in (".env", ".flaskenv"): 

704 path = dotenv.find_dotenv(name, usecwd=True) 

705 

706 if not path: 

707 continue 

708 

709 dotenv.load_dotenv(path, encoding="utf-8") 

710 loaded = True 

711 

712 return loaded # True if at least one file was located and loaded. 

713 

714 

715def show_server_banner(debug, app_import_path): 

716 """Show extra startup messages the first time the server is run, 

717 ignoring the reloader. 

718 """ 

719 if is_running_from_reloader(): 

720 return 

721 

722 if app_import_path is not None: 

723 click.echo(f" * Serving Flask app '{app_import_path}'") 

724 

725 if debug is not None: 

726 click.echo(f" * Debug mode: {'on' if debug else 'off'}") 

727 

728 

729class CertParamType(click.ParamType): 

730 """Click option type for the ``--cert`` option. Allows either an 

731 existing file, the string ``'adhoc'``, or an import for a 

732 :class:`~ssl.SSLContext` object. 

733 """ 

734 

735 name = "path" 

736 

737 def __init__(self): 

738 self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) 

739 

740 def convert(self, value, param, ctx): 

741 try: 

742 import ssl 

743 except ImportError: 

744 raise click.BadParameter( 

745 'Using "--cert" requires Python to be compiled with SSL support.', 

746 ctx, 

747 param, 

748 ) from None 

749 

750 try: 

751 return self.path_type(value, param, ctx) 

752 except click.BadParameter: 

753 value = click.STRING(value, param, ctx).lower() 

754 

755 if value == "adhoc": 

756 try: 

757 import cryptography # noqa: F401 

758 except ImportError: 

759 raise click.BadParameter( 

760 "Using ad-hoc certificates requires the cryptography library.", 

761 ctx, 

762 param, 

763 ) from None 

764 

765 return value 

766 

767 obj = import_string(value, silent=True) 

768 

769 if isinstance(obj, ssl.SSLContext): 

770 return obj 

771 

772 raise 

773 

774 

775def _validate_key(ctx, param, value): 

776 """The ``--key`` option must be specified when ``--cert`` is a file. 

777 Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed. 

778 """ 

779 cert = ctx.params.get("cert") 

780 is_adhoc = cert == "adhoc" 

781 

782 try: 

783 import ssl 

784 except ImportError: 

785 is_context = False 

786 else: 

787 is_context = isinstance(cert, ssl.SSLContext) 

788 

789 if value is not None: 

790 if is_adhoc: 

791 raise click.BadParameter( 

792 'When "--cert" is "adhoc", "--key" is not used.', ctx, param 

793 ) 

794 

795 if is_context: 

796 raise click.BadParameter( 

797 'When "--cert" is an SSLContext object, "--key is not used.', ctx, param 

798 ) 

799 

800 if not cert: 

801 raise click.BadParameter('"--cert" must also be specified.', ctx, param) 

802 

803 ctx.params["cert"] = cert, value 

804 

805 else: 

806 if cert and not (is_adhoc or is_context): 

807 raise click.BadParameter('Required when using "--cert".', ctx, param) 

808 

809 return value 

810 

811 

812class SeparatedPathType(click.Path): 

813 """Click option type that accepts a list of values separated by the 

814 OS's path separator (``:``, ``;`` on Windows). Each value is 

815 validated as a :class:`click.Path` type. 

816 """ 

817 

818 def convert(self, value, param, ctx): 

819 items = self.split_envvar_value(value) 

820 super_convert = super().convert 

821 return [super_convert(item, param, ctx) for item in items] 

822 

823 

824@click.command("run", short_help="Run a development server.") 

825@click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.") 

826@click.option("--port", "-p", default=5000, help="The port to bind to.") 

827@click.option( 

828 "--cert", 

829 type=CertParamType(), 

830 help="Specify a certificate file to use HTTPS.", 

831 is_eager=True, 

832) 

833@click.option( 

834 "--key", 

835 type=click.Path(exists=True, dir_okay=False, resolve_path=True), 

836 callback=_validate_key, 

837 expose_value=False, 

838 help="The key file to use when specifying a certificate.", 

839) 

840@click.option( 

841 "--reload/--no-reload", 

842 default=None, 

843 help="Enable or disable the reloader. By default the reloader " 

844 "is active if debug is enabled.", 

845) 

846@click.option( 

847 "--debugger/--no-debugger", 

848 default=None, 

849 help="Enable or disable the debugger. By default the debugger " 

850 "is active if debug is enabled.", 

851) 

852@click.option( 

853 "--with-threads/--without-threads", 

854 default=True, 

855 help="Enable or disable multithreading.", 

856) 

857@click.option( 

858 "--extra-files", 

859 default=None, 

860 type=SeparatedPathType(), 

861 help=( 

862 "Extra files that trigger a reload on change. Multiple paths" 

863 f" are separated by {os.path.pathsep!r}." 

864 ), 

865) 

866@click.option( 

867 "--exclude-patterns", 

868 default=None, 

869 type=SeparatedPathType(), 

870 help=( 

871 "Files matching these fnmatch patterns will not trigger a reload" 

872 " on change. Multiple patterns are separated by" 

873 f" {os.path.pathsep!r}." 

874 ), 

875) 

876@pass_script_info 

877def run_command( 

878 info, 

879 host, 

880 port, 

881 reload, 

882 debugger, 

883 with_threads, 

884 cert, 

885 extra_files, 

886 exclude_patterns, 

887): 

888 """Run a local development server. 

889 

890 This server is for development purposes only. It does not provide 

891 the stability, security, or performance of production WSGI servers. 

892 

893 The reloader and debugger are enabled by default with the '--debug' 

894 option. 

895 """ 

896 try: 

897 app = info.load_app() 

898 except Exception as e: 

899 if is_running_from_reloader(): 

900 # When reloading, print out the error immediately, but raise 

901 # it later so the debugger or server can handle it. 

902 traceback.print_exc() 

903 err = e 

904 

905 def app(environ, start_response): 

906 raise err from None 

907 

908 else: 

909 # When not reloading, raise the error immediately so the 

910 # command fails. 

911 raise e from None 

912 

913 debug = get_debug_flag() 

914 

915 if reload is None: 

916 reload = debug 

917 

918 if debugger is None: 

919 debugger = debug 

920 

921 show_server_banner(debug, info.app_import_path) 

922 

923 run_simple( 

924 host, 

925 port, 

926 app, 

927 use_reloader=reload, 

928 use_debugger=debugger, 

929 threaded=with_threads, 

930 ssl_context=cert, 

931 extra_files=extra_files, 

932 exclude_patterns=exclude_patterns, 

933 ) 

934 

935 

936run_command.params.insert(0, _debug_option) 

937 

938 

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

940@with_appcontext 

941def shell_command() -> None: 

942 """Run an interactive Python shell in the context of a given 

943 Flask application. The application will populate the default 

944 namespace of this shell according to its configuration. 

945 

946 This is useful for executing small snippets of management code 

947 without having to manually configure the application. 

948 """ 

949 import code 

950 

951 banner = ( 

952 f"Python {sys.version} on {sys.platform}\n" 

953 f"App: {current_app.import_name}\n" 

954 f"Instance: {current_app.instance_path}" 

955 ) 

956 ctx: dict = {} 

957 

958 # Support the regular Python interpreter startup script if someone 

959 # is using it. 

960 startup = os.environ.get("PYTHONSTARTUP") 

961 if startup and os.path.isfile(startup): 

962 with open(startup) as f: 

963 eval(compile(f.read(), startup, "exec"), ctx) 

964 

965 ctx.update(current_app.make_shell_context()) 

966 

967 # Site, customize, or startup script can set a hook to call when 

968 # entering interactive mode. The default one sets up readline with 

969 # tab and history completion. 

970 interactive_hook = getattr(sys, "__interactivehook__", None) 

971 

972 if interactive_hook is not None: 

973 try: 

974 import readline 

975 from rlcompleter import Completer 

976 except ImportError: 

977 pass 

978 else: 

979 # rlcompleter uses __main__.__dict__ by default, which is 

980 # flask.__main__. Use the shell context instead. 

981 readline.set_completer(Completer(ctx).complete) 

982 

983 interactive_hook() 

984 

985 code.interact(banner=banner, local=ctx) 

986 

987 

988@click.command("routes", short_help="Show the routes for the app.") 

989@click.option( 

990 "--sort", 

991 "-s", 

992 type=click.Choice(("endpoint", "methods", "rule", "match")), 

993 default="endpoint", 

994 help=( 

995 'Method to sort routes by. "match" is the order that Flask will match ' 

996 "routes when dispatching a request." 

997 ), 

998) 

999@click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.") 

1000@with_appcontext 

1001def routes_command(sort: str, all_methods: bool) -> None: 

1002 """Show all registered routes with endpoints and methods.""" 

1003 

1004 rules = list(current_app.url_map.iter_rules()) 

1005 if not rules: 

1006 click.echo("No routes were registered.") 

1007 return 

1008 

1009 ignored_methods = set(() if all_methods else ("HEAD", "OPTIONS")) 

1010 

1011 if sort in ("endpoint", "rule"): 

1012 rules = sorted(rules, key=attrgetter(sort)) 

1013 elif sort == "methods": 

1014 rules = sorted(rules, key=lambda rule: sorted(rule.methods)) # type: ignore 

1015 

1016 rule_methods = [ 

1017 ", ".join(sorted(rule.methods - ignored_methods)) # type: ignore 

1018 for rule in rules 

1019 ] 

1020 

1021 headers = ("Endpoint", "Methods", "Rule") 

1022 widths = ( 

1023 max(len(rule.endpoint) for rule in rules), 

1024 max(len(methods) for methods in rule_methods), 

1025 max(len(rule.rule) for rule in rules), 

1026 ) 

1027 widths = [max(len(h), w) for h, w in zip(headers, widths)] 

1028 row = "{{0:<{0}}} {{1:<{1}}} {{2:<{2}}}".format(*widths) 

1029 

1030 click.echo(row.format(*headers).strip()) 

1031 click.echo(row.format(*("-" * width for width in widths))) 

1032 

1033 for rule, methods in zip(rules, rule_methods): 

1034 click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip()) 

1035 

1036 

1037cli = FlaskGroup( 

1038 name="flask", 

1039 help="""\ 

1040A general utility script for Flask applications. 

1041 

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

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

1044in the current directory. 

1045""", 

1046) 

1047 

1048 

1049def main() -> None: 

1050 cli.main() 

1051 

1052 

1053if __name__ == "__main__": 

1054 main()