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