Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/flask/sansio/blueprints.py: 29%
226 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-22 06:29 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-22 06:29 +0000
1from __future__ import annotations
3import os
4import typing as t
5from collections import defaultdict
6from functools import update_wrapper
8from .. import typing as ft
9from .scaffold import _endpoint_from_view_func
10from .scaffold import _sentinel
11from .scaffold import Scaffold
12from .scaffold import setupmethod
14if t.TYPE_CHECKING: # pragma: no cover
15 from .app import App
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)
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 """
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
51 #: a reference to the blueprint that created this setup state.
52 self.blueprint = blueprint
54 #: a dictionary with all options that were passed to the
55 #: :meth:`~flask.Flask.register_blueprint` method.
56 self.options = options
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
64 subdomain = self.options.get("subdomain")
65 if subdomain is None:
66 subdomain = self.blueprint.subdomain
68 #: The subdomain that the blueprint should be active for, ``None``
69 #: otherwise.
70 self.subdomain = subdomain
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
79 self.name = self.options.get("name", blueprint.name)
80 self.name_prefix = self.options.get("name_prefix", "")
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", ()))
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"))
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 )
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.
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.
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.
133 See :doc:`/blueprints` for more information.
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.
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.
169 .. versionadded:: 0.7
170 """
172 _got_registered_once = False
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 )
195 if not name:
196 raise ValueError("'name' may not be empty.")
198 if "." in name:
199 raise ValueError("'name' may not contain a dot '.' character.")
201 self.name = name
202 self.url_prefix = url_prefix
203 self.subdomain = subdomain
204 self.deferred_functions: list[DeferredSetupFunction] = []
206 if url_defaults is None:
207 url_defaults = {}
209 self.url_values_defaults = url_defaults
210 self.cli_group = cli_group
211 self._blueprints: list[tuple[Blueprint, dict[str, t.Any]]] = []
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 )
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)
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 """
240 def wrapper(state: BlueprintSetupState) -> None:
241 if state.first_registration:
242 func(state)
244 self.record(update_wrapper(wrapper, func))
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)
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.
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``.
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))
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.
279 :param app: The application this blueprint is being registered
280 with.
281 :param options: Keyword arguments forwarded from
282 :meth:`~Flask.register_blueprint`.
284 .. versionchanged:: 2.3
285 Nested blueprints now correctly apply subdomains.
287 .. versionchanged:: 2.1
288 Registering the same blueprint with the same name multiple
289 times is an error.
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.
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(".")
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 ""
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 )
316 first_bp_registration = not any(bp is self for bp in app.blueprints.values())
317 first_name_registration = name not in app.blueprints
319 app.blueprints[name] = self
320 self._got_registered_once = True
321 state = self.make_setup_state(app, options, first_bp_registration)
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 )
330 # Merge blueprint data into parent.
331 if first_bp_registration or first_name_registration:
332 self._merge_blueprint_funcs(app, name)
334 for deferred in self.deferred_functions:
335 deferred(state)
337 cli_resolved_group = options.get("cli_group", self.cli_group)
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)
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")
354 if bp_subdomain is None:
355 bp_subdomain = blueprint.subdomain
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
364 if bp_url_prefix is None:
365 bp_url_prefix = blueprint.url_prefix
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
376 bp_options["name_prefix"] = name
377 blueprint.register(app, bp_options)
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)
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
399 for endpoint, func in self.view_functions.items():
400 app.view_functions[endpoint] = func
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)
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.
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.")
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.")
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 )
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`.
450 :param name: the optional name of the filter, otherwise the
451 function name will be used.
452 """
454 def decorator(f: T_template_filter) -> T_template_filter:
455 self.add_app_template_filter(f, name=name)
456 return f
458 return decorator
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`.
468 :param name: the optional name of the filter, otherwise the
469 function name will be used.
470 """
472 def register_template(state: BlueprintSetupState) -> None:
473 state.app.jinja_env.filters[name or f.__name__] = f
475 self.record_once(register_template)
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`.
484 .. versionadded:: 0.10
486 :param name: the optional name of the test, otherwise the
487 function name will be used.
488 """
490 def decorator(f: T_template_test) -> T_template_test:
491 self.add_app_template_test(f, name=name)
492 return f
494 return decorator
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`.
504 .. versionadded:: 0.10
506 :param name: the optional name of the test, otherwise the
507 function name will be used.
508 """
510 def register_template(state: BlueprintSetupState) -> None:
511 state.app.jinja_env.tests[name or f.__name__] = f
513 self.record_once(register_template)
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`.
522 .. versionadded:: 0.10
524 :param name: the optional name of the global, otherwise the
525 function name will be used.
526 """
528 def decorator(f: T_template_global) -> T_template_global:
529 self.add_app_template_global(f, name=name)
530 return f
532 return decorator
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`.
542 .. versionadded:: 0.10
544 :param name: the optional name of the global, otherwise the
545 function name will be used.
546 """
548 def register_template(state: BlueprintSetupState) -> None:
549 state.app.jinja_env.globals[name or f.__name__] = f
551 self.record_once(register_template)
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
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
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
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
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 """
603 def decorator(f: T_error_handler) -> T_error_handler:
604 def from_blueprint(state: BlueprintSetupState) -> None:
605 state.app.errorhandler(code)(f)
607 self.record_once(from_blueprint)
608 return f
610 return decorator
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
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