Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/flask/sansio/blueprints.py: 29%

224 statements  

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

1from __future__ import annotations 

2 

3import os 

4import typing as t 

5from collections import defaultdict 

6from functools import update_wrapper 

7 

8from .. import typing as ft 

9from .scaffold import _endpoint_from_view_func 

10from .scaffold import _sentinel 

11from .scaffold import Scaffold 

12from .scaffold import setupmethod 

13 

14if t.TYPE_CHECKING: # pragma: no cover 

15 from .app import App 

16 

17DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable] 

18T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable) 

19T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) 

20T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) 

21T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) 

22T_template_context_processor = t.TypeVar( 

23 "T_template_context_processor", bound=ft.TemplateContextProcessorCallable 

24) 

25T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) 

26T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) 

27T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) 

28T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) 

29T_url_value_preprocessor = t.TypeVar( 

30 "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable 

31) 

32 

33 

34class BlueprintSetupState: 

35 """Temporary holder object for registering a blueprint with the 

36 application. An instance of this class is created by the 

37 :meth:`~flask.Blueprint.make_setup_state` method and later passed 

38 to all register callback functions. 

39 """ 

40 

41 def __init__( 

42 self, 

43 blueprint: Blueprint, 

44 app: App, 

45 options: t.Any, 

46 first_registration: bool, 

47 ) -> None: 

48 #: a reference to the current application 

49 self.app = app 

50 

51 #: a reference to the blueprint that created this setup state. 

52 self.blueprint = blueprint 

53 

54 #: a dictionary with all options that were passed to the 

55 #: :meth:`~flask.Flask.register_blueprint` method. 

56 self.options = options 

57 

58 #: as blueprints can be registered multiple times with the 

59 #: application and not everything wants to be registered 

60 #: multiple times on it, this attribute can be used to figure 

61 #: out if the blueprint was registered in the past already. 

62 self.first_registration = first_registration 

63 

64 subdomain = self.options.get("subdomain") 

65 if subdomain is None: 

66 subdomain = self.blueprint.subdomain 

67 

68 #: The subdomain that the blueprint should be active for, ``None`` 

69 #: otherwise. 

70 self.subdomain = subdomain 

71 

72 url_prefix = self.options.get("url_prefix") 

73 if url_prefix is None: 

74 url_prefix = self.blueprint.url_prefix 

75 #: The prefix that should be used for all URLs defined on the 

76 #: blueprint. 

77 self.url_prefix = url_prefix 

78 

79 self.name = self.options.get("name", blueprint.name) 

80 self.name_prefix = self.options.get("name_prefix", "") 

81 

82 #: A dictionary with URL defaults that is added to each and every 

83 #: URL that was defined with the blueprint. 

84 self.url_defaults = dict(self.blueprint.url_values_defaults) 

85 self.url_defaults.update(self.options.get("url_defaults", ())) 

86 

87 def add_url_rule( 

88 self, 

89 rule: str, 

90 endpoint: str | None = None, 

91 view_func: t.Callable | None = None, 

92 **options: t.Any, 

93 ) -> None: 

94 """A helper method to register a rule (and optionally a view function) 

95 to the application. The endpoint is automatically prefixed with the 

96 blueprint's name. 

97 """ 

98 if self.url_prefix is not None: 

99 if rule: 

100 rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/"))) 

101 else: 

102 rule = self.url_prefix 

103 options.setdefault("subdomain", self.subdomain) 

104 if endpoint is None: 

105 endpoint = _endpoint_from_view_func(view_func) # type: ignore 

106 defaults = self.url_defaults 

107 if "defaults" in options: 

108 defaults = dict(defaults, **options.pop("defaults")) 

109 

110 self.app.add_url_rule( 

111 rule, 

112 f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."), 

113 view_func, 

114 defaults=defaults, 

115 **options, 

116 ) 

117 

118 

119class Blueprint(Scaffold): 

120 """Represents a blueprint, a collection of routes and other 

121 app-related functions that can be registered on a real application 

122 later. 

123 

124 A blueprint is an object that allows defining application functions 

125 without requiring an application object ahead of time. It uses the 

126 same decorators as :class:`~flask.Flask`, but defers the need for an 

127 application by recording them for later registration. 

128 

129 Decorating a function with a blueprint creates a deferred function 

130 that is called with :class:`~flask.blueprints.BlueprintSetupState` 

131 when the blueprint is registered on an application. 

132 

133 See :doc:`/blueprints` for more information. 

134 

135 :param name: The name of the blueprint. Will be prepended to each 

136 endpoint name. 

137 :param import_name: The name of the blueprint package, usually 

138 ``__name__``. This helps locate the ``root_path`` for the 

139 blueprint. 

140 :param static_folder: A folder with static files that should be 

141 served by the blueprint's static route. The path is relative to 

142 the blueprint's root path. Blueprint static files are disabled 

143 by default. 

144 :param static_url_path: The url to serve static files from. 

145 Defaults to ``static_folder``. If the blueprint does not have 

146 a ``url_prefix``, the app's static route will take precedence, 

147 and the blueprint's static files won't be accessible. 

148 :param template_folder: A folder with templates that should be added 

149 to the app's template search path. The path is relative to the 

150 blueprint's root path. Blueprint templates are disabled by 

151 default. Blueprint templates have a lower precedence than those 

152 in the app's templates folder. 

153 :param url_prefix: A path to prepend to all of the blueprint's URLs, 

154 to make them distinct from the rest of the app's routes. 

155 :param subdomain: A subdomain that blueprint routes will match on by 

156 default. 

157 :param url_defaults: A dict of default values that blueprint routes 

158 will receive by default. 

159 :param root_path: By default, the blueprint will automatically set 

160 this based on ``import_name``. In certain situations this 

161 automatic detection can fail, so the path can be specified 

162 manually instead. 

163 

164 .. versionchanged:: 1.1.0 

165 Blueprints have a ``cli`` group to register nested CLI commands. 

166 The ``cli_group`` parameter controls the name of the group under 

167 the ``flask`` command. 

168 

169 .. versionadded:: 0.7 

170 """ 

171 

172 _got_registered_once = False 

173 

174 def __init__( 

175 self, 

176 name: str, 

177 import_name: str, 

178 static_folder: str | os.PathLike | None = None, 

179 static_url_path: str | None = None, 

180 template_folder: str | os.PathLike | None = None, 

181 url_prefix: str | None = None, 

182 subdomain: str | None = None, 

183 url_defaults: dict | None = None, 

184 root_path: str | None = None, 

185 cli_group: str | None = _sentinel, # type: ignore 

186 ): 

187 super().__init__( 

188 import_name=import_name, 

189 static_folder=static_folder, 

190 static_url_path=static_url_path, 

191 template_folder=template_folder, 

192 root_path=root_path, 

193 ) 

194 

195 if not name: 

196 raise ValueError("'name' may not be empty.") 

197 

198 if "." in name: 

199 raise ValueError("'name' may not contain a dot '.' character.") 

200 

201 self.name = name 

202 self.url_prefix = url_prefix 

203 self.subdomain = subdomain 

204 self.deferred_functions: list[DeferredSetupFunction] = [] 

205 

206 if url_defaults is None: 

207 url_defaults = {} 

208 

209 self.url_values_defaults = url_defaults 

210 self.cli_group = cli_group 

211 self._blueprints: list[tuple[Blueprint, dict]] = [] 

212 

213 def _check_setup_finished(self, f_name: str) -> None: 

214 if self._got_registered_once: 

215 raise AssertionError( 

216 f"The setup method '{f_name}' can no longer be called on the blueprint" 

217 f" '{self.name}'. It has already been registered at least once, any" 

218 " changes will not be applied consistently.\n" 

219 "Make sure all imports, decorators, functions, etc. needed to set up" 

220 " the blueprint are done before registering it." 

221 ) 

222 

223 @setupmethod 

224 def record(self, func: t.Callable) -> None: 

225 """Registers a function that is called when the blueprint is 

226 registered on the application. This function is called with the 

227 state as argument as returned by the :meth:`make_setup_state` 

228 method. 

229 """ 

230 self.deferred_functions.append(func) 

231 

232 @setupmethod 

233 def record_once(self, func: t.Callable) -> None: 

234 """Works like :meth:`record` but wraps the function in another 

235 function that will ensure the function is only called once. If the 

236 blueprint is registered a second time on the application, the 

237 function passed is not called. 

238 """ 

239 

240 def wrapper(state: BlueprintSetupState) -> None: 

241 if state.first_registration: 

242 func(state) 

243 

244 self.record(update_wrapper(wrapper, func)) 

245 

246 def make_setup_state( 

247 self, app: App, options: dict, first_registration: bool = False 

248 ) -> BlueprintSetupState: 

249 """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` 

250 object that is later passed to the register callback functions. 

251 Subclasses can override this to return a subclass of the setup state. 

252 """ 

253 return BlueprintSetupState(self, app, options, first_registration) 

254 

255 @setupmethod 

256 def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: 

257 """Register a :class:`~flask.Blueprint` on this blueprint. Keyword 

258 arguments passed to this method will override the defaults set 

259 on the blueprint. 

260 

261 .. versionchanged:: 2.0.1 

262 The ``name`` option can be used to change the (pre-dotted) 

263 name the blueprint is registered with. This allows the same 

264 blueprint to be registered multiple times with unique names 

265 for ``url_for``. 

266 

267 .. versionadded:: 2.0 

268 """ 

269 if blueprint is self: 

270 raise ValueError("Cannot register a blueprint on itself") 

271 self._blueprints.append((blueprint, options)) 

272 

273 def register(self, app: App, options: dict) -> None: 

274 """Called by :meth:`Flask.register_blueprint` to register all 

275 views and callbacks registered on the blueprint with the 

276 application. Creates a :class:`.BlueprintSetupState` and calls 

277 each :meth:`record` callback with it. 

278 

279 :param app: The application this blueprint is being registered 

280 with. 

281 :param options: Keyword arguments forwarded from 

282 :meth:`~Flask.register_blueprint`. 

283 

284 .. versionchanged:: 2.3 

285 Nested blueprints now correctly apply subdomains. 

286 

287 .. versionchanged:: 2.1 

288 Registering the same blueprint with the same name multiple 

289 times is an error. 

290 

291 .. versionchanged:: 2.0.1 

292 Nested blueprints are registered with their dotted name. 

293 This allows different blueprints with the same name to be 

294 nested at different locations. 

295 

296 .. versionchanged:: 2.0.1 

297 The ``name`` option can be used to change the (pre-dotted) 

298 name the blueprint is registered with. This allows the same 

299 blueprint to be registered multiple times with unique names 

300 for ``url_for``. 

301 """ 

302 name_prefix = options.get("name_prefix", "") 

303 self_name = options.get("name", self.name) 

304 name = f"{name_prefix}.{self_name}".lstrip(".") 

305 

306 if name in app.blueprints: 

307 bp_desc = "this" if app.blueprints[name] is self else "a different" 

308 existing_at = f" '{name}'" if self_name != name else "" 

309 

310 raise ValueError( 

311 f"The name '{self_name}' is already registered for" 

312 f" {bp_desc} blueprint{existing_at}. Use 'name=' to" 

313 f" provide a unique name." 

314 ) 

315 

316 first_bp_registration = not any(bp is self for bp in app.blueprints.values()) 

317 first_name_registration = name not in app.blueprints 

318 

319 app.blueprints[name] = self 

320 self._got_registered_once = True 

321 state = self.make_setup_state(app, options, first_bp_registration) 

322 

323 if self.has_static_folder: 

324 state.add_url_rule( 

325 f"{self.static_url_path}/<path:filename>", 

326 view_func=self.send_static_file, # type: ignore[attr-defined] 

327 endpoint="static", 

328 ) 

329 

330 # Merge blueprint data into parent. 

331 if first_bp_registration or first_name_registration: 

332 self._merge_blueprint_funcs(app, name) 

333 

334 for deferred in self.deferred_functions: 

335 deferred(state) 

336 

337 cli_resolved_group = options.get("cli_group", self.cli_group) 

338 

339 if self.cli.commands: 

340 if cli_resolved_group is None: 

341 app.cli.commands.update(self.cli.commands) 

342 elif cli_resolved_group is _sentinel: 

343 self.cli.name = name 

344 app.cli.add_command(self.cli) 

345 else: 

346 self.cli.name = cli_resolved_group 

347 app.cli.add_command(self.cli) 

348 

349 for blueprint, bp_options in self._blueprints: 

350 bp_options = bp_options.copy() 

351 bp_url_prefix = bp_options.get("url_prefix") 

352 bp_subdomain = bp_options.get("subdomain") 

353 

354 if bp_subdomain is None: 

355 bp_subdomain = blueprint.subdomain 

356 

357 if state.subdomain is not None and bp_subdomain is not None: 

358 bp_options["subdomain"] = bp_subdomain + "." + state.subdomain 

359 elif bp_subdomain is not None: 

360 bp_options["subdomain"] = bp_subdomain 

361 elif state.subdomain is not None: 

362 bp_options["subdomain"] = state.subdomain 

363 

364 if bp_url_prefix is None: 

365 bp_url_prefix = blueprint.url_prefix 

366 

367 if state.url_prefix is not None and bp_url_prefix is not None: 

368 bp_options["url_prefix"] = ( 

369 state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") 

370 ) 

371 elif bp_url_prefix is not None: 

372 bp_options["url_prefix"] = bp_url_prefix 

373 elif state.url_prefix is not None: 

374 bp_options["url_prefix"] = state.url_prefix 

375 

376 bp_options["name_prefix"] = name 

377 blueprint.register(app, bp_options) 

378 

379 def _merge_blueprint_funcs(self, app: App, name: str) -> None: 

380 def extend(bp_dict, parent_dict): 

381 for key, values in bp_dict.items(): 

382 key = name if key is None else f"{name}.{key}" 

383 parent_dict[key].extend(values) 

384 

385 for key, value in self.error_handler_spec.items(): 

386 key = name if key is None else f"{name}.{key}" 

387 value = defaultdict( 

388 dict, 

389 { 

390 code: {exc_class: func for exc_class, func in code_values.items()} 

391 for code, code_values in value.items() 

392 }, 

393 ) 

394 app.error_handler_spec[key] = value 

395 

396 for endpoint, func in self.view_functions.items(): 

397 app.view_functions[endpoint] = func 

398 

399 extend(self.before_request_funcs, app.before_request_funcs) 

400 extend(self.after_request_funcs, app.after_request_funcs) 

401 extend( 

402 self.teardown_request_funcs, 

403 app.teardown_request_funcs, 

404 ) 

405 extend(self.url_default_functions, app.url_default_functions) 

406 extend(self.url_value_preprocessors, app.url_value_preprocessors) 

407 extend(self.template_context_processors, app.template_context_processors) 

408 

409 @setupmethod 

410 def add_url_rule( 

411 self, 

412 rule: str, 

413 endpoint: str | None = None, 

414 view_func: ft.RouteCallable | None = None, 

415 provide_automatic_options: bool | None = None, 

416 **options: t.Any, 

417 ) -> None: 

418 """Register a URL rule with the blueprint. See :meth:`.Flask.add_url_rule` for 

419 full documentation. 

420 

421 The URL rule is prefixed with the blueprint's URL prefix. The endpoint name, 

422 used with :func:`url_for`, is prefixed with the blueprint's name. 

423 """ 

424 if endpoint and "." in endpoint: 

425 raise ValueError("'endpoint' may not contain a dot '.' character.") 

426 

427 if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: 

428 raise ValueError("'view_func' name may not contain a dot '.' character.") 

429 

430 self.record( 

431 lambda s: s.add_url_rule( 

432 rule, 

433 endpoint, 

434 view_func, 

435 provide_automatic_options=provide_automatic_options, 

436 **options, 

437 ) 

438 ) 

439 

440 @setupmethod 

441 def app_template_filter( 

442 self, name: str | None = None 

443 ) -> t.Callable[[T_template_filter], T_template_filter]: 

444 """Register a template filter, available in any template rendered by the 

445 application. Equivalent to :meth:`.Flask.template_filter`. 

446 

447 :param name: the optional name of the filter, otherwise the 

448 function name will be used. 

449 """ 

450 

451 def decorator(f: T_template_filter) -> T_template_filter: 

452 self.add_app_template_filter(f, name=name) 

453 return f 

454 

455 return decorator 

456 

457 @setupmethod 

458 def add_app_template_filter( 

459 self, f: ft.TemplateFilterCallable, name: str | None = None 

460 ) -> None: 

461 """Register a template filter, available in any template rendered by the 

462 application. Works like the :meth:`app_template_filter` decorator. Equivalent to 

463 :meth:`.Flask.add_template_filter`. 

464 

465 :param name: the optional name of the filter, otherwise the 

466 function name will be used. 

467 """ 

468 

469 def register_template(state: BlueprintSetupState) -> None: 

470 state.app.jinja_env.filters[name or f.__name__] = f 

471 

472 self.record_once(register_template) 

473 

474 @setupmethod 

475 def app_template_test( 

476 self, name: str | None = None 

477 ) -> t.Callable[[T_template_test], T_template_test]: 

478 """Register a template test, available in any template rendered by the 

479 application. Equivalent to :meth:`.Flask.template_test`. 

480 

481 .. versionadded:: 0.10 

482 

483 :param name: the optional name of the test, otherwise the 

484 function name will be used. 

485 """ 

486 

487 def decorator(f: T_template_test) -> T_template_test: 

488 self.add_app_template_test(f, name=name) 

489 return f 

490 

491 return decorator 

492 

493 @setupmethod 

494 def add_app_template_test( 

495 self, f: ft.TemplateTestCallable, name: str | None = None 

496 ) -> None: 

497 """Register a template test, available in any template rendered by the 

498 application. Works like the :meth:`app_template_test` decorator. Equivalent to 

499 :meth:`.Flask.add_template_test`. 

500 

501 .. versionadded:: 0.10 

502 

503 :param name: the optional name of the test, otherwise the 

504 function name will be used. 

505 """ 

506 

507 def register_template(state: BlueprintSetupState) -> None: 

508 state.app.jinja_env.tests[name or f.__name__] = f 

509 

510 self.record_once(register_template) 

511 

512 @setupmethod 

513 def app_template_global( 

514 self, name: str | None = None 

515 ) -> t.Callable[[T_template_global], T_template_global]: 

516 """Register a template global, available in any template rendered by the 

517 application. Equivalent to :meth:`.Flask.template_global`. 

518 

519 .. versionadded:: 0.10 

520 

521 :param name: the optional name of the global, otherwise the 

522 function name will be used. 

523 """ 

524 

525 def decorator(f: T_template_global) -> T_template_global: 

526 self.add_app_template_global(f, name=name) 

527 return f 

528 

529 return decorator 

530 

531 @setupmethod 

532 def add_app_template_global( 

533 self, f: ft.TemplateGlobalCallable, name: str | None = None 

534 ) -> None: 

535 """Register a template global, available in any template rendered by the 

536 application. Works like the :meth:`app_template_global` decorator. Equivalent to 

537 :meth:`.Flask.add_template_global`. 

538 

539 .. versionadded:: 0.10 

540 

541 :param name: the optional name of the global, otherwise the 

542 function name will be used. 

543 """ 

544 

545 def register_template(state: BlueprintSetupState) -> None: 

546 state.app.jinja_env.globals[name or f.__name__] = f 

547 

548 self.record_once(register_template) 

549 

550 @setupmethod 

551 def before_app_request(self, f: T_before_request) -> T_before_request: 

552 """Like :meth:`before_request`, but before every request, not only those handled 

553 by the blueprint. Equivalent to :meth:`.Flask.before_request`. 

554 """ 

555 self.record_once( 

556 lambda s: s.app.before_request_funcs.setdefault(None, []).append(f) 

557 ) 

558 return f 

559 

560 @setupmethod 

561 def after_app_request(self, f: T_after_request) -> T_after_request: 

562 """Like :meth:`after_request`, but after every request, not only those handled 

563 by the blueprint. Equivalent to :meth:`.Flask.after_request`. 

564 """ 

565 self.record_once( 

566 lambda s: s.app.after_request_funcs.setdefault(None, []).append(f) 

567 ) 

568 return f 

569 

570 @setupmethod 

571 def teardown_app_request(self, f: T_teardown) -> T_teardown: 

572 """Like :meth:`teardown_request`, but after every request, not only those 

573 handled by the blueprint. Equivalent to :meth:`.Flask.teardown_request`. 

574 """ 

575 self.record_once( 

576 lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f) 

577 ) 

578 return f 

579 

580 @setupmethod 

581 def app_context_processor( 

582 self, f: T_template_context_processor 

583 ) -> T_template_context_processor: 

584 """Like :meth:`context_processor`, but for templates rendered by every view, not 

585 only by the blueprint. Equivalent to :meth:`.Flask.context_processor`. 

586 """ 

587 self.record_once( 

588 lambda s: s.app.template_context_processors.setdefault(None, []).append(f) 

589 ) 

590 return f 

591 

592 @setupmethod 

593 def app_errorhandler( 

594 self, code: type[Exception] | int 

595 ) -> t.Callable[[T_error_handler], T_error_handler]: 

596 """Like :meth:`errorhandler`, but for every request, not only those handled by 

597 the blueprint. Equivalent to :meth:`.Flask.errorhandler`. 

598 """ 

599 

600 def decorator(f: T_error_handler) -> T_error_handler: 

601 self.record_once(lambda s: s.app.errorhandler(code)(f)) 

602 return f 

603 

604 return decorator 

605 

606 @setupmethod 

607 def app_url_value_preprocessor( 

608 self, f: T_url_value_preprocessor 

609 ) -> T_url_value_preprocessor: 

610 """Like :meth:`url_value_preprocessor`, but for every request, not only those 

611 handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`. 

612 """ 

613 self.record_once( 

614 lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) 

615 ) 

616 return f 

617 

618 @setupmethod 

619 def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults: 

620 """Like :meth:`url_defaults`, but for every request, not only those handled by 

621 the blueprint. Equivalent to :meth:`.Flask.url_defaults`. 

622 """ 

623 self.record_once( 

624 lambda s: s.app.url_default_functions.setdefault(None, []).append(f) 

625 ) 

626 return f