Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/flask/blueprints.py: 32%
241 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
1import json
2import os
3import typing as t
4from collections import defaultdict
5from functools import update_wrapper
7from . import typing as ft
8from .scaffold import _endpoint_from_view_func
9from .scaffold import _sentinel
10from .scaffold import Scaffold
11from .scaffold import setupmethod
13if t.TYPE_CHECKING: # pragma: no cover
14 from .app import Flask
16DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
17T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable)
18T_before_first_request = t.TypeVar(
19 "T_before_first_request", bound=ft.BeforeFirstRequestCallable
20)
21T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable)
22T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable)
23T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable)
24T_template_context_processor = t.TypeVar(
25 "T_template_context_processor", bound=ft.TemplateContextProcessorCallable
26)
27T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable)
28T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable)
29T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable)
30T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable)
31T_url_value_preprocessor = t.TypeVar(
32 "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable
33)
36class BlueprintSetupState:
37 """Temporary holder object for registering a blueprint with the
38 application. An instance of this class is created by the
39 :meth:`~flask.Blueprint.make_setup_state` method and later passed
40 to all register callback functions.
41 """
43 def __init__(
44 self,
45 blueprint: "Blueprint",
46 app: "Flask",
47 options: t.Any,
48 first_registration: bool,
49 ) -> None:
50 #: a reference to the current application
51 self.app = app
53 #: a reference to the blueprint that created this setup state.
54 self.blueprint = blueprint
56 #: a dictionary with all options that were passed to the
57 #: :meth:`~flask.Flask.register_blueprint` method.
58 self.options = options
60 #: as blueprints can be registered multiple times with the
61 #: application and not everything wants to be registered
62 #: multiple times on it, this attribute can be used to figure
63 #: out if the blueprint was registered in the past already.
64 self.first_registration = first_registration
66 subdomain = self.options.get("subdomain")
67 if subdomain is None:
68 subdomain = self.blueprint.subdomain
70 #: The subdomain that the blueprint should be active for, ``None``
71 #: otherwise.
72 self.subdomain = subdomain
74 url_prefix = self.options.get("url_prefix")
75 if url_prefix is None:
76 url_prefix = self.blueprint.url_prefix
77 #: The prefix that should be used for all URLs defined on the
78 #: blueprint.
79 self.url_prefix = url_prefix
81 self.name = self.options.get("name", blueprint.name)
82 self.name_prefix = self.options.get("name_prefix", "")
84 #: A dictionary with URL defaults that is added to each and every
85 #: URL that was defined with the blueprint.
86 self.url_defaults = dict(self.blueprint.url_values_defaults)
87 self.url_defaults.update(self.options.get("url_defaults", ()))
89 def add_url_rule(
90 self,
91 rule: str,
92 endpoint: t.Optional[str] = None,
93 view_func: t.Optional[t.Callable] = None,
94 **options: t.Any,
95 ) -> None:
96 """A helper method to register a rule (and optionally a view function)
97 to the application. The endpoint is automatically prefixed with the
98 blueprint's name.
99 """
100 if self.url_prefix is not None:
101 if rule:
102 rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/")))
103 else:
104 rule = self.url_prefix
105 options.setdefault("subdomain", self.subdomain)
106 if endpoint is None:
107 endpoint = _endpoint_from_view_func(view_func) # type: ignore
108 defaults = self.url_defaults
109 if "defaults" in options:
110 defaults = dict(defaults, **options.pop("defaults"))
112 self.app.add_url_rule(
113 rule,
114 f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."),
115 view_func,
116 defaults=defaults,
117 **options,
118 )
121class Blueprint(Scaffold):
122 """Represents a blueprint, a collection of routes and other
123 app-related functions that can be registered on a real application
124 later.
126 A blueprint is an object that allows defining application functions
127 without requiring an application object ahead of time. It uses the
128 same decorators as :class:`~flask.Flask`, but defers the need for an
129 application by recording them for later registration.
131 Decorating a function with a blueprint creates a deferred function
132 that is called with :class:`~flask.blueprints.BlueprintSetupState`
133 when the blueprint is registered on an application.
135 See :doc:`/blueprints` for more information.
137 :param name: The name of the blueprint. Will be prepended to each
138 endpoint name.
139 :param import_name: The name of the blueprint package, usually
140 ``__name__``. This helps locate the ``root_path`` for the
141 blueprint.
142 :param static_folder: A folder with static files that should be
143 served by the blueprint's static route. The path is relative to
144 the blueprint's root path. Blueprint static files are disabled
145 by default.
146 :param static_url_path: The url to serve static files from.
147 Defaults to ``static_folder``. If the blueprint does not have
148 a ``url_prefix``, the app's static route will take precedence,
149 and the blueprint's static files won't be accessible.
150 :param template_folder: A folder with templates that should be added
151 to the app's template search path. The path is relative to the
152 blueprint's root path. Blueprint templates are disabled by
153 default. Blueprint templates have a lower precedence than those
154 in the app's templates folder.
155 :param url_prefix: A path to prepend to all of the blueprint's URLs,
156 to make them distinct from the rest of the app's routes.
157 :param subdomain: A subdomain that blueprint routes will match on by
158 default.
159 :param url_defaults: A dict of default values that blueprint routes
160 will receive by default.
161 :param root_path: By default, the blueprint will automatically set
162 this based on ``import_name``. In certain situations this
163 automatic detection can fail, so the path can be specified
164 manually instead.
166 .. versionchanged:: 1.1.0
167 Blueprints have a ``cli`` group to register nested CLI commands.
168 The ``cli_group`` parameter controls the name of the group under
169 the ``flask`` command.
171 .. versionadded:: 0.7
172 """
174 _got_registered_once = False
176 _json_encoder: t.Union[t.Type[json.JSONEncoder], None] = None
177 _json_decoder: t.Union[t.Type[json.JSONDecoder], None] = None
179 @property
180 def json_encoder(
181 self,
182 ) -> t.Union[t.Type[json.JSONEncoder], None]:
183 """Blueprint-local JSON encoder class to use. Set to ``None`` to use the app's.
185 .. deprecated:: 2.2
186 Will be removed in Flask 2.3. Customize
187 :attr:`json_provider_class` instead.
189 .. versionadded:: 0.10
190 """
191 import warnings
193 warnings.warn(
194 "'bp.json_encoder' is deprecated and will be removed in Flask 2.3."
195 " Customize 'app.json_provider_class' or 'app.json' instead.",
196 DeprecationWarning,
197 stacklevel=2,
198 )
199 return self._json_encoder
201 @json_encoder.setter
202 def json_encoder(self, value: t.Union[t.Type[json.JSONEncoder], None]) -> None:
203 import warnings
205 warnings.warn(
206 "'bp.json_encoder' is deprecated and will be removed in Flask 2.3."
207 " Customize 'app.json_provider_class' or 'app.json' instead.",
208 DeprecationWarning,
209 stacklevel=2,
210 )
211 self._json_encoder = value
213 @property
214 def json_decoder(
215 self,
216 ) -> t.Union[t.Type[json.JSONDecoder], None]:
217 """Blueprint-local JSON decoder class to use. Set to ``None`` to use the app's.
219 .. deprecated:: 2.2
220 Will be removed in Flask 2.3. Customize
221 :attr:`json_provider_class` instead.
223 .. versionadded:: 0.10
224 """
225 import warnings
227 warnings.warn(
228 "'bp.json_decoder' is deprecated and will be removed in Flask 2.3."
229 " Customize 'app.json_provider_class' or 'app.json' instead.",
230 DeprecationWarning,
231 stacklevel=2,
232 )
233 return self._json_decoder
235 @json_decoder.setter
236 def json_decoder(self, value: t.Union[t.Type[json.JSONDecoder], None]) -> None:
237 import warnings
239 warnings.warn(
240 "'bp.json_decoder' is deprecated and will be removed in Flask 2.3."
241 " Customize 'app.json_provider_class' or 'app.json' instead.",
242 DeprecationWarning,
243 stacklevel=2,
244 )
245 self._json_decoder = value
247 def __init__(
248 self,
249 name: str,
250 import_name: str,
251 static_folder: t.Optional[t.Union[str, os.PathLike]] = None,
252 static_url_path: t.Optional[str] = None,
253 template_folder: t.Optional[t.Union[str, os.PathLike]] = None,
254 url_prefix: t.Optional[str] = None,
255 subdomain: t.Optional[str] = None,
256 url_defaults: t.Optional[dict] = None,
257 root_path: t.Optional[str] = None,
258 cli_group: t.Optional[str] = _sentinel, # type: ignore
259 ):
260 super().__init__(
261 import_name=import_name,
262 static_folder=static_folder,
263 static_url_path=static_url_path,
264 template_folder=template_folder,
265 root_path=root_path,
266 )
268 if "." in name:
269 raise ValueError("'name' may not contain a dot '.' character.")
271 self.name = name
272 self.url_prefix = url_prefix
273 self.subdomain = subdomain
274 self.deferred_functions: t.List[DeferredSetupFunction] = []
276 if url_defaults is None:
277 url_defaults = {}
279 self.url_values_defaults = url_defaults
280 self.cli_group = cli_group
281 self._blueprints: t.List[t.Tuple["Blueprint", dict]] = []
283 def _check_setup_finished(self, f_name: str) -> None:
284 if self._got_registered_once:
285 import warnings
287 warnings.warn(
288 f"The setup method '{f_name}' can no longer be called on"
289 f" the blueprint '{self.name}'. It has already been"
290 " registered at least once, any changes will not be"
291 " applied consistently.\n"
292 "Make sure all imports, decorators, functions, etc."
293 " needed to set up the blueprint are done before"
294 " registering it.\n"
295 "This warning will become an exception in Flask 2.3.",
296 UserWarning,
297 stacklevel=3,
298 )
300 @setupmethod
301 def record(self, func: t.Callable) -> None:
302 """Registers a function that is called when the blueprint is
303 registered on the application. This function is called with the
304 state as argument as returned by the :meth:`make_setup_state`
305 method.
306 """
307 self.deferred_functions.append(func)
309 @setupmethod
310 def record_once(self, func: t.Callable) -> None:
311 """Works like :meth:`record` but wraps the function in another
312 function that will ensure the function is only called once. If the
313 blueprint is registered a second time on the application, the
314 function passed is not called.
315 """
317 def wrapper(state: BlueprintSetupState) -> None:
318 if state.first_registration:
319 func(state)
321 self.record(update_wrapper(wrapper, func))
323 def make_setup_state(
324 self, app: "Flask", options: dict, first_registration: bool = False
325 ) -> BlueprintSetupState:
326 """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState`
327 object that is later passed to the register callback functions.
328 Subclasses can override this to return a subclass of the setup state.
329 """
330 return BlueprintSetupState(self, app, options, first_registration)
332 @setupmethod
333 def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None:
334 """Register a :class:`~flask.Blueprint` on this blueprint. Keyword
335 arguments passed to this method will override the defaults set
336 on the blueprint.
338 .. versionchanged:: 2.0.1
339 The ``name`` option can be used to change the (pre-dotted)
340 name the blueprint is registered with. This allows the same
341 blueprint to be registered multiple times with unique names
342 for ``url_for``.
344 .. versionadded:: 2.0
345 """
346 if blueprint is self:
347 raise ValueError("Cannot register a blueprint on itself")
348 self._blueprints.append((blueprint, options))
350 def register(self, app: "Flask", options: dict) -> None:
351 """Called by :meth:`Flask.register_blueprint` to register all
352 views and callbacks registered on the blueprint with the
353 application. Creates a :class:`.BlueprintSetupState` and calls
354 each :meth:`record` callback with it.
356 :param app: The application this blueprint is being registered
357 with.
358 :param options: Keyword arguments forwarded from
359 :meth:`~Flask.register_blueprint`.
361 .. versionchanged:: 2.0.1
362 Nested blueprints are registered with their dotted name.
363 This allows different blueprints with the same name to be
364 nested at different locations.
366 .. versionchanged:: 2.0.1
367 The ``name`` option can be used to change the (pre-dotted)
368 name the blueprint is registered with. This allows the same
369 blueprint to be registered multiple times with unique names
370 for ``url_for``.
372 .. versionchanged:: 2.0.1
373 Registering the same blueprint with the same name multiple
374 times is deprecated and will become an error in Flask 2.1.
375 """
376 name_prefix = options.get("name_prefix", "")
377 self_name = options.get("name", self.name)
378 name = f"{name_prefix}.{self_name}".lstrip(".")
380 if name in app.blueprints:
381 bp_desc = "this" if app.blueprints[name] is self else "a different"
382 existing_at = f" '{name}'" if self_name != name else ""
384 raise ValueError(
385 f"The name '{self_name}' is already registered for"
386 f" {bp_desc} blueprint{existing_at}. Use 'name=' to"
387 f" provide a unique name."
388 )
390 first_bp_registration = not any(bp is self for bp in app.blueprints.values())
391 first_name_registration = name not in app.blueprints
393 app.blueprints[name] = self
394 self._got_registered_once = True
395 state = self.make_setup_state(app, options, first_bp_registration)
397 if self.has_static_folder:
398 state.add_url_rule(
399 f"{self.static_url_path}/<path:filename>",
400 view_func=self.send_static_file,
401 endpoint="static",
402 )
404 # Merge blueprint data into parent.
405 if first_bp_registration or first_name_registration:
407 def extend(bp_dict, parent_dict):
408 for key, values in bp_dict.items():
409 key = name if key is None else f"{name}.{key}"
410 parent_dict[key].extend(values)
412 for key, value in self.error_handler_spec.items():
413 key = name if key is None else f"{name}.{key}"
414 value = defaultdict(
415 dict,
416 {
417 code: {
418 exc_class: func for exc_class, func in code_values.items()
419 }
420 for code, code_values in value.items()
421 },
422 )
423 app.error_handler_spec[key] = value
425 for endpoint, func in self.view_functions.items():
426 app.view_functions[endpoint] = func
428 extend(self.before_request_funcs, app.before_request_funcs)
429 extend(self.after_request_funcs, app.after_request_funcs)
430 extend(
431 self.teardown_request_funcs,
432 app.teardown_request_funcs,
433 )
434 extend(self.url_default_functions, app.url_default_functions)
435 extend(self.url_value_preprocessors, app.url_value_preprocessors)
436 extend(self.template_context_processors, app.template_context_processors)
438 for deferred in self.deferred_functions:
439 deferred(state)
441 cli_resolved_group = options.get("cli_group", self.cli_group)
443 if self.cli.commands:
444 if cli_resolved_group is None:
445 app.cli.commands.update(self.cli.commands)
446 elif cli_resolved_group is _sentinel:
447 self.cli.name = name
448 app.cli.add_command(self.cli)
449 else:
450 self.cli.name = cli_resolved_group
451 app.cli.add_command(self.cli)
453 for blueprint, bp_options in self._blueprints:
454 bp_options = bp_options.copy()
455 bp_url_prefix = bp_options.get("url_prefix")
457 if bp_url_prefix is None:
458 bp_url_prefix = blueprint.url_prefix
460 if state.url_prefix is not None and bp_url_prefix is not None:
461 bp_options["url_prefix"] = (
462 state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/")
463 )
464 elif bp_url_prefix is not None:
465 bp_options["url_prefix"] = bp_url_prefix
466 elif state.url_prefix is not None:
467 bp_options["url_prefix"] = state.url_prefix
469 bp_options["name_prefix"] = name
470 blueprint.register(app, bp_options)
472 @setupmethod
473 def add_url_rule(
474 self,
475 rule: str,
476 endpoint: t.Optional[str] = None,
477 view_func: t.Optional[ft.RouteCallable] = None,
478 provide_automatic_options: t.Optional[bool] = None,
479 **options: t.Any,
480 ) -> None:
481 """Register a URL rule with the blueprint. See :meth:`.Flask.add_url_rule` for
482 full documentation.
484 The URL rule is prefixed with the blueprint's URL prefix. The endpoint name,
485 used with :func:`url_for`, is prefixed with the blueprint's name.
486 """
487 if endpoint and "." in endpoint:
488 raise ValueError("'endpoint' may not contain a dot '.' character.")
490 if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__:
491 raise ValueError("'view_func' name may not contain a dot '.' character.")
493 self.record(
494 lambda s: s.add_url_rule(
495 rule,
496 endpoint,
497 view_func,
498 provide_automatic_options=provide_automatic_options,
499 **options,
500 )
501 )
503 @setupmethod
504 def app_template_filter(
505 self, name: t.Optional[str] = None
506 ) -> t.Callable[[T_template_filter], T_template_filter]:
507 """Register a template filter, available in any template rendered by the
508 application. Equivalent to :meth:`.Flask.template_filter`.
510 :param name: the optional name of the filter, otherwise the
511 function name will be used.
512 """
514 def decorator(f: T_template_filter) -> T_template_filter:
515 self.add_app_template_filter(f, name=name)
516 return f
518 return decorator
520 @setupmethod
521 def add_app_template_filter(
522 self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None
523 ) -> None:
524 """Register a template filter, available in any template rendered by the
525 application. Works like the :meth:`app_template_filter` decorator. Equivalent to
526 :meth:`.Flask.add_template_filter`.
528 :param name: the optional name of the filter, otherwise the
529 function name will be used.
530 """
532 def register_template(state: BlueprintSetupState) -> None:
533 state.app.jinja_env.filters[name or f.__name__] = f
535 self.record_once(register_template)
537 @setupmethod
538 def app_template_test(
539 self, name: t.Optional[str] = None
540 ) -> t.Callable[[T_template_test], T_template_test]:
541 """Register a template test, available in any template rendered by the
542 application. Equivalent to :meth:`.Flask.template_test`.
544 .. versionadded:: 0.10
546 :param name: the optional name of the test, otherwise the
547 function name will be used.
548 """
550 def decorator(f: T_template_test) -> T_template_test:
551 self.add_app_template_test(f, name=name)
552 return f
554 return decorator
556 @setupmethod
557 def add_app_template_test(
558 self, f: ft.TemplateTestCallable, name: t.Optional[str] = None
559 ) -> None:
560 """Register a template test, available in any template rendered by the
561 application. Works like the :meth:`app_template_test` decorator. Equivalent to
562 :meth:`.Flask.add_template_test`.
564 .. versionadded:: 0.10
566 :param name: the optional name of the test, otherwise the
567 function name will be used.
568 """
570 def register_template(state: BlueprintSetupState) -> None:
571 state.app.jinja_env.tests[name or f.__name__] = f
573 self.record_once(register_template)
575 @setupmethod
576 def app_template_global(
577 self, name: t.Optional[str] = None
578 ) -> t.Callable[[T_template_global], T_template_global]:
579 """Register a template global, available in any template rendered by the
580 application. Equivalent to :meth:`.Flask.template_global`.
582 .. versionadded:: 0.10
584 :param name: the optional name of the global, otherwise the
585 function name will be used.
586 """
588 def decorator(f: T_template_global) -> T_template_global:
589 self.add_app_template_global(f, name=name)
590 return f
592 return decorator
594 @setupmethod
595 def add_app_template_global(
596 self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None
597 ) -> None:
598 """Register a template global, available in any template rendered by the
599 application. Works like the :meth:`app_template_global` decorator. Equivalent to
600 :meth:`.Flask.add_template_global`.
602 .. versionadded:: 0.10
604 :param name: the optional name of the global, otherwise the
605 function name will be used.
606 """
608 def register_template(state: BlueprintSetupState) -> None:
609 state.app.jinja_env.globals[name or f.__name__] = f
611 self.record_once(register_template)
613 @setupmethod
614 def before_app_request(self, f: T_before_request) -> T_before_request:
615 """Like :meth:`before_request`, but before every request, not only those handled
616 by the blueprint. Equivalent to :meth:`.Flask.before_request`.
617 """
618 self.record_once(
619 lambda s: s.app.before_request_funcs.setdefault(None, []).append(f)
620 )
621 return f
623 @setupmethod
624 def before_app_first_request(
625 self, f: T_before_first_request
626 ) -> T_before_first_request:
627 """Register a function to run before the first request to the application is
628 handled by the worker. Equivalent to :meth:`.Flask.before_first_request`.
630 .. deprecated:: 2.2
631 Will be removed in Flask 2.3. Run setup code when creating
632 the application instead.
633 """
634 import warnings
636 warnings.warn(
637 "'before_app_first_request' is deprecated and will be"
638 " removed in Flask 2.3. Use 'record_once' instead to run"
639 " setup code when registering the blueprint.",
640 DeprecationWarning,
641 stacklevel=2,
642 )
643 self.record_once(lambda s: s.app.before_first_request_funcs.append(f))
644 return f
646 @setupmethod
647 def after_app_request(self, f: T_after_request) -> T_after_request:
648 """Like :meth:`after_request`, but after every request, not only those handled
649 by the blueprint. Equivalent to :meth:`.Flask.after_request`.
650 """
651 self.record_once(
652 lambda s: s.app.after_request_funcs.setdefault(None, []).append(f)
653 )
654 return f
656 @setupmethod
657 def teardown_app_request(self, f: T_teardown) -> T_teardown:
658 """Like :meth:`teardown_request`, but after every request, not only those
659 handled by the blueprint. Equivalent to :meth:`.Flask.teardown_request`.
660 """
661 self.record_once(
662 lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f)
663 )
664 return f
666 @setupmethod
667 def app_context_processor(
668 self, f: T_template_context_processor
669 ) -> T_template_context_processor:
670 """Like :meth:`context_processor`, but for templates rendered by every view, not
671 only by the blueprint. Equivalent to :meth:`.Flask.context_processor`.
672 """
673 self.record_once(
674 lambda s: s.app.template_context_processors.setdefault(None, []).append(f)
675 )
676 return f
678 @setupmethod
679 def app_errorhandler(
680 self, code: t.Union[t.Type[Exception], int]
681 ) -> t.Callable[[T_error_handler], T_error_handler]:
682 """Like :meth:`errorhandler`, but for every request, not only those handled by
683 the blueprint. Equivalent to :meth:`.Flask.errorhandler`.
684 """
686 def decorator(f: T_error_handler) -> T_error_handler:
687 self.record_once(lambda s: s.app.errorhandler(code)(f))
688 return f
690 return decorator
692 @setupmethod
693 def app_url_value_preprocessor(
694 self, f: T_url_value_preprocessor
695 ) -> T_url_value_preprocessor:
696 """Like :meth:`url_value_preprocessor`, but for every request, not only those
697 handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`.
698 """
699 self.record_once(
700 lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f)
701 )
702 return f
704 @setupmethod
705 def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults:
706 """Like :meth:`url_defaults`, but for every request, not only those handled by
707 the blueprint. Equivalent to :meth:`.Flask.url_defaults`.
708 """
709 self.record_once(
710 lambda s: s.app.url_default_functions.setdefault(None, []).append(f)
711 )
712 return f