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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

226 statements  

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"], None] 

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

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: ft.RouteCallable | 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[str] | None = None, 

179 static_url_path: str | None = None, 

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

181 url_prefix: str | None = None, 

182 subdomain: str | None = None, 

183 url_defaults: dict[str, t.Any] | None = None, 

184 root_path: str | None = None, 

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

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[str, t.Any]]] = [] 

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: DeferredSetupFunction) -> 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: DeferredSetupFunction) -> 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[str, t.Any], 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[str, t.Any]) -> 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( 

381 bp_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], 

382 parent_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], 

383 ) -> None: 

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

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

386 parent_dict[key].extend(values) 

387 

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

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

390 value = defaultdict( 

391 dict, 

392 { 

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

394 for code, code_values in value.items() 

395 }, 

396 ) 

397 app.error_handler_spec[key] = value 

398 

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

400 app.view_functions[endpoint] = func 

401 

402 extend(self.before_request_funcs, app.before_request_funcs) 

403 extend(self.after_request_funcs, app.after_request_funcs) 

404 extend( 

405 self.teardown_request_funcs, 

406 app.teardown_request_funcs, 

407 ) 

408 extend(self.url_default_functions, app.url_default_functions) 

409 extend(self.url_value_preprocessors, app.url_value_preprocessors) 

410 extend(self.template_context_processors, app.template_context_processors) 

411 

412 @setupmethod 

413 def add_url_rule( 

414 self, 

415 rule: str, 

416 endpoint: str | None = None, 

417 view_func: ft.RouteCallable | None = None, 

418 provide_automatic_options: bool | None = None, 

419 **options: t.Any, 

420 ) -> None: 

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

422 full documentation. 

423 

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

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

426 """ 

427 if endpoint and "." in endpoint: 

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

429 

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

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

432 

433 self.record( 

434 lambda s: s.add_url_rule( 

435 rule, 

436 endpoint, 

437 view_func, 

438 provide_automatic_options=provide_automatic_options, 

439 **options, 

440 ) 

441 ) 

442 

443 @setupmethod 

444 def app_template_filter( 

445 self, name: str | None = None 

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

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

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

449 

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

451 function name will be used. 

452 """ 

453 

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

455 self.add_app_template_filter(f, name=name) 

456 return f 

457 

458 return decorator 

459 

460 @setupmethod 

461 def add_app_template_filter( 

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

463 ) -> None: 

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

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

466 :meth:`.Flask.add_template_filter`. 

467 

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

469 function name will be used. 

470 """ 

471 

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

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

474 

475 self.record_once(register_template) 

476 

477 @setupmethod 

478 def app_template_test( 

479 self, name: str | None = None 

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

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

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

483 

484 .. versionadded:: 0.10 

485 

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

487 function name will be used. 

488 """ 

489 

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

491 self.add_app_template_test(f, name=name) 

492 return f 

493 

494 return decorator 

495 

496 @setupmethod 

497 def add_app_template_test( 

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

499 ) -> None: 

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

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

502 :meth:`.Flask.add_template_test`. 

503 

504 .. versionadded:: 0.10 

505 

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

507 function name will be used. 

508 """ 

509 

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

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

512 

513 self.record_once(register_template) 

514 

515 @setupmethod 

516 def app_template_global( 

517 self, name: str | None = None 

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

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

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

521 

522 .. versionadded:: 0.10 

523 

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

525 function name will be used. 

526 """ 

527 

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

529 self.add_app_template_global(f, name=name) 

530 return f 

531 

532 return decorator 

533 

534 @setupmethod 

535 def add_app_template_global( 

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

537 ) -> None: 

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

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

540 :meth:`.Flask.add_template_global`. 

541 

542 .. versionadded:: 0.10 

543 

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

545 function name will be used. 

546 """ 

547 

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

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

550 

551 self.record_once(register_template) 

552 

553 @setupmethod 

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

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

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

557 """ 

558 self.record_once( 

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

560 ) 

561 return f 

562 

563 @setupmethod 

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

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

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

567 """ 

568 self.record_once( 

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

570 ) 

571 return f 

572 

573 @setupmethod 

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

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

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

577 """ 

578 self.record_once( 

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

580 ) 

581 return f 

582 

583 @setupmethod 

584 def app_context_processor( 

585 self, f: T_template_context_processor 

586 ) -> T_template_context_processor: 

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

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

589 """ 

590 self.record_once( 

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

592 ) 

593 return f 

594 

595 @setupmethod 

596 def app_errorhandler( 

597 self, code: type[Exception] | int 

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

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

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

601 """ 

602 

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

604 def from_blueprint(state: BlueprintSetupState) -> None: 

605 state.app.errorhandler(code)(f) 

606 

607 self.record_once(from_blueprint) 

608 return f 

609 

610 return decorator 

611 

612 @setupmethod 

613 def app_url_value_preprocessor( 

614 self, f: T_url_value_preprocessor 

615 ) -> T_url_value_preprocessor: 

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

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

618 """ 

619 self.record_once( 

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

621 ) 

622 return f 

623 

624 @setupmethod 

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

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

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

628 """ 

629 self.record_once( 

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

631 ) 

632 return f