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
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-09 07:17 +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"], 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)
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: 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"))
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 | 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 )
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]] = []
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: 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)
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 """
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, 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) -> 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(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)
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
396 for endpoint, func in self.view_functions.items():
397 app.view_functions[endpoint] = func
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)
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.
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.")
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.")
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 )
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`.
447 :param name: the optional name of the filter, otherwise the
448 function name will be used.
449 """
451 def decorator(f: T_template_filter) -> T_template_filter:
452 self.add_app_template_filter(f, name=name)
453 return f
455 return decorator
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`.
465 :param name: the optional name of the filter, otherwise the
466 function name will be used.
467 """
469 def register_template(state: BlueprintSetupState) -> None:
470 state.app.jinja_env.filters[name or f.__name__] = f
472 self.record_once(register_template)
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`.
481 .. versionadded:: 0.10
483 :param name: the optional name of the test, otherwise the
484 function name will be used.
485 """
487 def decorator(f: T_template_test) -> T_template_test:
488 self.add_app_template_test(f, name=name)
489 return f
491 return decorator
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`.
501 .. versionadded:: 0.10
503 :param name: the optional name of the test, otherwise the
504 function name will be used.
505 """
507 def register_template(state: BlueprintSetupState) -> None:
508 state.app.jinja_env.tests[name or f.__name__] = f
510 self.record_once(register_template)
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`.
519 .. versionadded:: 0.10
521 :param name: the optional name of the global, otherwise the
522 function name will be used.
523 """
525 def decorator(f: T_template_global) -> T_template_global:
526 self.add_app_template_global(f, name=name)
527 return f
529 return decorator
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`.
539 .. versionadded:: 0.10
541 :param name: the optional name of the global, otherwise the
542 function name will be used.
543 """
545 def register_template(state: BlueprintSetupState) -> None:
546 state.app.jinja_env.globals[name or f.__name__] = f
548 self.record_once(register_template)
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
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
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
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
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 """
600 def decorator(f: T_error_handler) -> T_error_handler:
601 self.record_once(lambda s: s.app.errorhandler(code)(f))
602 return f
604 return decorator
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
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