Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/flask/blueprints.py: 30%
192 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:03 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:03 +0000
1import os
2import typing as t
3from collections import defaultdict
4from functools import update_wrapper
6from . import typing as ft
7from .scaffold import _endpoint_from_view_func
8from .scaffold import _sentinel
9from .scaffold import Scaffold
11if t.TYPE_CHECKING:
12 from .app import Flask
14DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
17class BlueprintSetupState:
18 """Temporary holder object for registering a blueprint with the
19 application. An instance of this class is created by the
20 :meth:`~flask.Blueprint.make_setup_state` method and later passed
21 to all register callback functions.
22 """
24 def __init__(
25 self,
26 blueprint: "Blueprint",
27 app: "Flask",
28 options: t.Any,
29 first_registration: bool,
30 ) -> None:
31 #: a reference to the current application
32 self.app = app
34 #: a reference to the blueprint that created this setup state.
35 self.blueprint = blueprint
37 #: a dictionary with all options that were passed to the
38 #: :meth:`~flask.Flask.register_blueprint` method.
39 self.options = options
41 #: as blueprints can be registered multiple times with the
42 #: application and not everything wants to be registered
43 #: multiple times on it, this attribute can be used to figure
44 #: out if the blueprint was registered in the past already.
45 self.first_registration = first_registration
47 subdomain = self.options.get("subdomain")
48 if subdomain is None:
49 subdomain = self.blueprint.subdomain
51 #: The subdomain that the blueprint should be active for, ``None``
52 #: otherwise.
53 self.subdomain = subdomain
55 url_prefix = self.options.get("url_prefix")
56 if url_prefix is None:
57 url_prefix = self.blueprint.url_prefix
58 #: The prefix that should be used for all URLs defined on the
59 #: blueprint.
60 self.url_prefix = url_prefix
62 self.name = self.options.get("name", blueprint.name)
63 self.name_prefix = self.options.get("name_prefix", "")
65 #: A dictionary with URL defaults that is added to each and every
66 #: URL that was defined with the blueprint.
67 self.url_defaults = dict(self.blueprint.url_values_defaults)
68 self.url_defaults.update(self.options.get("url_defaults", ()))
70 def add_url_rule(
71 self,
72 rule: str,
73 endpoint: t.Optional[str] = None,
74 view_func: t.Optional[t.Callable] = None,
75 **options: t.Any,
76 ) -> None:
77 """A helper method to register a rule (and optionally a view function)
78 to the application. The endpoint is automatically prefixed with the
79 blueprint's name.
80 """
81 if self.url_prefix is not None:
82 if rule:
83 rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/")))
84 else:
85 rule = self.url_prefix
86 options.setdefault("subdomain", self.subdomain)
87 if endpoint is None:
88 endpoint = _endpoint_from_view_func(view_func) # type: ignore
89 defaults = self.url_defaults
90 if "defaults" in options:
91 defaults = dict(defaults, **options.pop("defaults"))
93 self.app.add_url_rule(
94 rule,
95 f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."),
96 view_func,
97 defaults=defaults,
98 **options,
99 )
102class Blueprint(Scaffold):
103 """Represents a blueprint, a collection of routes and other
104 app-related functions that can be registered on a real application
105 later.
107 A blueprint is an object that allows defining application functions
108 without requiring an application object ahead of time. It uses the
109 same decorators as :class:`~flask.Flask`, but defers the need for an
110 application by recording them for later registration.
112 Decorating a function with a blueprint creates a deferred function
113 that is called with :class:`~flask.blueprints.BlueprintSetupState`
114 when the blueprint is registered on an application.
116 See :doc:`/blueprints` for more information.
118 :param name: The name of the blueprint. Will be prepended to each
119 endpoint name.
120 :param import_name: The name of the blueprint package, usually
121 ``__name__``. This helps locate the ``root_path`` for the
122 blueprint.
123 :param static_folder: A folder with static files that should be
124 served by the blueprint's static route. The path is relative to
125 the blueprint's root path. Blueprint static files are disabled
126 by default.
127 :param static_url_path: The url to serve static files from.
128 Defaults to ``static_folder``. If the blueprint does not have
129 a ``url_prefix``, the app's static route will take precedence,
130 and the blueprint's static files won't be accessible.
131 :param template_folder: A folder with templates that should be added
132 to the app's template search path. The path is relative to the
133 blueprint's root path. Blueprint templates are disabled by
134 default. Blueprint templates have a lower precedence than those
135 in the app's templates folder.
136 :param url_prefix: A path to prepend to all of the blueprint's URLs,
137 to make them distinct from the rest of the app's routes.
138 :param subdomain: A subdomain that blueprint routes will match on by
139 default.
140 :param url_defaults: A dict of default values that blueprint routes
141 will receive by default.
142 :param root_path: By default, the blueprint will automatically set
143 this based on ``import_name``. In certain situations this
144 automatic detection can fail, so the path can be specified
145 manually instead.
147 .. versionchanged:: 1.1.0
148 Blueprints have a ``cli`` group to register nested CLI commands.
149 The ``cli_group`` parameter controls the name of the group under
150 the ``flask`` command.
152 .. versionadded:: 0.7
153 """
155 warn_on_modifications = False
156 _got_registered_once = False
158 #: Blueprint local JSON encoder class to use. Set to ``None`` to use
159 #: the app's :class:`~flask.Flask.json_encoder`.
160 json_encoder = None
161 #: Blueprint local JSON decoder class to use. Set to ``None`` to use
162 #: the app's :class:`~flask.Flask.json_decoder`.
163 json_decoder = None
165 def __init__(
166 self,
167 name: str,
168 import_name: str,
169 static_folder: t.Optional[t.Union[str, os.PathLike]] = None,
170 static_url_path: t.Optional[str] = None,
171 template_folder: t.Optional[str] = None,
172 url_prefix: t.Optional[str] = None,
173 subdomain: t.Optional[str] = None,
174 url_defaults: t.Optional[dict] = None,
175 root_path: t.Optional[str] = None,
176 cli_group: t.Optional[str] = _sentinel, # type: ignore
177 ):
178 super().__init__(
179 import_name=import_name,
180 static_folder=static_folder,
181 static_url_path=static_url_path,
182 template_folder=template_folder,
183 root_path=root_path,
184 )
186 if "." in name:
187 raise ValueError("'name' may not contain a dot '.' character.")
189 self.name = name
190 self.url_prefix = url_prefix
191 self.subdomain = subdomain
192 self.deferred_functions: t.List[DeferredSetupFunction] = []
194 if url_defaults is None:
195 url_defaults = {}
197 self.url_values_defaults = url_defaults
198 self.cli_group = cli_group
199 self._blueprints: t.List[t.Tuple["Blueprint", dict]] = []
201 def _is_setup_finished(self) -> bool:
202 return self.warn_on_modifications and self._got_registered_once
204 def record(self, func: t.Callable) -> None:
205 """Registers a function that is called when the blueprint is
206 registered on the application. This function is called with the
207 state as argument as returned by the :meth:`make_setup_state`
208 method.
209 """
210 if self._got_registered_once and self.warn_on_modifications:
211 from warnings import warn
213 warn(
214 Warning(
215 "The blueprint was already registered once but is"
216 " getting modified now. These changes will not show"
217 " up."
218 )
219 )
220 self.deferred_functions.append(func)
222 def record_once(self, func: t.Callable) -> None:
223 """Works like :meth:`record` but wraps the function in another
224 function that will ensure the function is only called once. If the
225 blueprint is registered a second time on the application, the
226 function passed is not called.
227 """
229 def wrapper(state: BlueprintSetupState) -> None:
230 if state.first_registration:
231 func(state)
233 return self.record(update_wrapper(wrapper, func))
235 def make_setup_state(
236 self, app: "Flask", options: dict, first_registration: bool = False
237 ) -> BlueprintSetupState:
238 """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState`
239 object that is later passed to the register callback functions.
240 Subclasses can override this to return a subclass of the setup state.
241 """
242 return BlueprintSetupState(self, app, options, first_registration)
244 def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None:
245 """Register a :class:`~flask.Blueprint` on this blueprint. Keyword
246 arguments passed to this method will override the defaults set
247 on the blueprint.
249 .. versionchanged:: 2.0.1
250 The ``name`` option can be used to change the (pre-dotted)
251 name the blueprint is registered with. This allows the same
252 blueprint to be registered multiple times with unique names
253 for ``url_for``.
255 .. versionadded:: 2.0
256 """
257 if blueprint is self:
258 raise ValueError("Cannot register a blueprint on itself")
259 self._blueprints.append((blueprint, options))
261 def register(self, app: "Flask", options: dict) -> None:
262 """Called by :meth:`Flask.register_blueprint` to register all
263 views and callbacks registered on the blueprint with the
264 application. Creates a :class:`.BlueprintSetupState` and calls
265 each :meth:`record` callback with it.
267 :param app: The application this blueprint is being registered
268 with.
269 :param options: Keyword arguments forwarded from
270 :meth:`~Flask.register_blueprint`.
272 .. versionchanged:: 2.0.1
273 Nested blueprints are registered with their dotted name.
274 This allows different blueprints with the same name to be
275 nested at different locations.
277 .. versionchanged:: 2.0.1
278 The ``name`` option can be used to change the (pre-dotted)
279 name the blueprint is registered with. This allows the same
280 blueprint to be registered multiple times with unique names
281 for ``url_for``.
283 .. versionchanged:: 2.0.1
284 Registering the same blueprint with the same name multiple
285 times is deprecated and will become an error in Flask 2.1.
286 """
287 name_prefix = options.get("name_prefix", "")
288 self_name = options.get("name", self.name)
289 name = f"{name_prefix}.{self_name}".lstrip(".")
291 if name in app.blueprints:
292 bp_desc = "this" if app.blueprints[name] is self else "a different"
293 existing_at = f" '{name}'" if self_name != name else ""
295 raise ValueError(
296 f"The name '{self_name}' is already registered for"
297 f" {bp_desc} blueprint{existing_at}. Use 'name=' to"
298 f" provide a unique name."
299 )
301 first_bp_registration = not any(bp is self for bp in app.blueprints.values())
302 first_name_registration = name not in app.blueprints
304 app.blueprints[name] = self
305 self._got_registered_once = True
306 state = self.make_setup_state(app, options, first_bp_registration)
308 if self.has_static_folder:
309 state.add_url_rule(
310 f"{self.static_url_path}/<path:filename>",
311 view_func=self.send_static_file,
312 endpoint="static",
313 )
315 # Merge blueprint data into parent.
316 if first_bp_registration or first_name_registration:
318 def extend(bp_dict, parent_dict):
319 for key, values in bp_dict.items():
320 key = name if key is None else f"{name}.{key}"
321 parent_dict[key].extend(values)
323 for key, value in self.error_handler_spec.items():
324 key = name if key is None else f"{name}.{key}"
325 value = defaultdict(
326 dict,
327 {
328 code: {
329 exc_class: func for exc_class, func in code_values.items()
330 }
331 for code, code_values in value.items()
332 },
333 )
334 app.error_handler_spec[key] = value
336 for endpoint, func in self.view_functions.items():
337 app.view_functions[endpoint] = func
339 extend(self.before_request_funcs, app.before_request_funcs)
340 extend(self.after_request_funcs, app.after_request_funcs)
341 extend(
342 self.teardown_request_funcs,
343 app.teardown_request_funcs,
344 )
345 extend(self.url_default_functions, app.url_default_functions)
346 extend(self.url_value_preprocessors, app.url_value_preprocessors)
347 extend(self.template_context_processors, app.template_context_processors)
349 for deferred in self.deferred_functions:
350 deferred(state)
352 cli_resolved_group = options.get("cli_group", self.cli_group)
354 if self.cli.commands:
355 if cli_resolved_group is None:
356 app.cli.commands.update(self.cli.commands)
357 elif cli_resolved_group is _sentinel:
358 self.cli.name = name
359 app.cli.add_command(self.cli)
360 else:
361 self.cli.name = cli_resolved_group
362 app.cli.add_command(self.cli)
364 for blueprint, bp_options in self._blueprints:
365 bp_options = bp_options.copy()
366 bp_url_prefix = bp_options.get("url_prefix")
368 if bp_url_prefix is None:
369 bp_url_prefix = blueprint.url_prefix
371 if state.url_prefix is not None and bp_url_prefix is not None:
372 bp_options["url_prefix"] = (
373 state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/")
374 )
375 elif bp_url_prefix is not None:
376 bp_options["url_prefix"] = bp_url_prefix
377 elif state.url_prefix is not None:
378 bp_options["url_prefix"] = state.url_prefix
380 bp_options["name_prefix"] = name
381 blueprint.register(app, bp_options)
383 def add_url_rule(
384 self,
385 rule: str,
386 endpoint: t.Optional[str] = None,
387 view_func: t.Optional[ft.ViewCallable] = None,
388 provide_automatic_options: t.Optional[bool] = None,
389 **options: t.Any,
390 ) -> None:
391 """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for
392 the :func:`url_for` function is prefixed with the name of the blueprint.
393 """
394 if endpoint and "." in endpoint:
395 raise ValueError("'endpoint' may not contain a dot '.' character.")
397 if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__:
398 raise ValueError("'view_func' name may not contain a dot '.' character.")
400 self.record(
401 lambda s: s.add_url_rule(
402 rule,
403 endpoint,
404 view_func,
405 provide_automatic_options=provide_automatic_options,
406 **options,
407 )
408 )
410 def app_template_filter(
411 self, name: t.Optional[str] = None
412 ) -> t.Callable[[ft.TemplateFilterCallable], ft.TemplateFilterCallable]:
413 """Register a custom template filter, available application wide. Like
414 :meth:`Flask.template_filter` but for a blueprint.
416 :param name: the optional name of the filter, otherwise the
417 function name will be used.
418 """
420 def decorator(f: ft.TemplateFilterCallable) -> ft.TemplateFilterCallable:
421 self.add_app_template_filter(f, name=name)
422 return f
424 return decorator
426 def add_app_template_filter(
427 self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None
428 ) -> None:
429 """Register a custom template filter, available application wide. Like
430 :meth:`Flask.add_template_filter` but for a blueprint. Works exactly
431 like the :meth:`app_template_filter` decorator.
433 :param name: the optional name of the filter, otherwise the
434 function name will be used.
435 """
437 def register_template(state: BlueprintSetupState) -> None:
438 state.app.jinja_env.filters[name or f.__name__] = f
440 self.record_once(register_template)
442 def app_template_test(
443 self, name: t.Optional[str] = None
444 ) -> t.Callable[[ft.TemplateTestCallable], ft.TemplateTestCallable]:
445 """Register a custom template test, available application wide. Like
446 :meth:`Flask.template_test` but for a blueprint.
448 .. versionadded:: 0.10
450 :param name: the optional name of the test, otherwise the
451 function name will be used.
452 """
454 def decorator(f: ft.TemplateTestCallable) -> ft.TemplateTestCallable:
455 self.add_app_template_test(f, name=name)
456 return f
458 return decorator
460 def add_app_template_test(
461 self, f: ft.TemplateTestCallable, name: t.Optional[str] = None
462 ) -> None:
463 """Register a custom template test, available application wide. Like
464 :meth:`Flask.add_template_test` but for a blueprint. Works exactly
465 like the :meth:`app_template_test` decorator.
467 .. versionadded:: 0.10
469 :param name: the optional name of the test, otherwise the
470 function name will be used.
471 """
473 def register_template(state: BlueprintSetupState) -> None:
474 state.app.jinja_env.tests[name or f.__name__] = f
476 self.record_once(register_template)
478 def app_template_global(
479 self, name: t.Optional[str] = None
480 ) -> t.Callable[[ft.TemplateGlobalCallable], ft.TemplateGlobalCallable]:
481 """Register a custom template global, available application wide. Like
482 :meth:`Flask.template_global` but for a blueprint.
484 .. versionadded:: 0.10
486 :param name: the optional name of the global, otherwise the
487 function name will be used.
488 """
490 def decorator(f: ft.TemplateGlobalCallable) -> ft.TemplateGlobalCallable:
491 self.add_app_template_global(f, name=name)
492 return f
494 return decorator
496 def add_app_template_global(
497 self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None
498 ) -> None:
499 """Register a custom template global, available application wide. Like
500 :meth:`Flask.add_template_global` but for a blueprint. Works exactly
501 like the :meth:`app_template_global` decorator.
503 .. versionadded:: 0.10
505 :param name: the optional name of the global, otherwise the
506 function name will be used.
507 """
509 def register_template(state: BlueprintSetupState) -> None:
510 state.app.jinja_env.globals[name or f.__name__] = f
512 self.record_once(register_template)
514 def before_app_request(
515 self, f: ft.BeforeRequestCallable
516 ) -> ft.BeforeRequestCallable:
517 """Like :meth:`Flask.before_request`. Such a function is executed
518 before each request, even if outside of a blueprint.
519 """
520 self.record_once(
521 lambda s: s.app.before_request_funcs.setdefault(None, []).append(f)
522 )
523 return f
525 def before_app_first_request(
526 self, f: ft.BeforeFirstRequestCallable
527 ) -> ft.BeforeFirstRequestCallable:
528 """Like :meth:`Flask.before_first_request`. Such a function is
529 executed before the first request to the application.
530 """
531 self.record_once(lambda s: s.app.before_first_request_funcs.append(f))
532 return f
534 def after_app_request(self, f: ft.AfterRequestCallable) -> ft.AfterRequestCallable:
535 """Like :meth:`Flask.after_request` but for a blueprint. Such a function
536 is executed after each request, even if outside of the blueprint.
537 """
538 self.record_once(
539 lambda s: s.app.after_request_funcs.setdefault(None, []).append(f)
540 )
541 return f
543 def teardown_app_request(self, f: ft.TeardownCallable) -> ft.TeardownCallable:
544 """Like :meth:`Flask.teardown_request` but for a blueprint. Such a
545 function is executed when tearing down each request, even if outside of
546 the blueprint.
547 """
548 self.record_once(
549 lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f)
550 )
551 return f
553 def app_context_processor(
554 self, f: ft.TemplateContextProcessorCallable
555 ) -> ft.TemplateContextProcessorCallable:
556 """Like :meth:`Flask.context_processor` but for a blueprint. Such a
557 function is executed each request, even if outside of the blueprint.
558 """
559 self.record_once(
560 lambda s: s.app.template_context_processors.setdefault(None, []).append(f)
561 )
562 return f
564 def app_errorhandler(
565 self, code: t.Union[t.Type[Exception], int]
566 ) -> t.Callable[[ft.ErrorHandlerDecorator], ft.ErrorHandlerDecorator]:
567 """Like :meth:`Flask.errorhandler` but for a blueprint. This
568 handler is used for all requests, even if outside of the blueprint.
569 """
571 def decorator(f: ft.ErrorHandlerDecorator) -> ft.ErrorHandlerDecorator:
572 self.record_once(lambda s: s.app.errorhandler(code)(f))
573 return f
575 return decorator
577 def app_url_value_preprocessor(
578 self, f: ft.URLValuePreprocessorCallable
579 ) -> ft.URLValuePreprocessorCallable:
580 """Same as :meth:`url_value_preprocessor` but application wide."""
581 self.record_once(
582 lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f)
583 )
584 return f
586 def app_url_defaults(self, f: ft.URLDefaultCallable) -> ft.URLDefaultCallable:
587 """Same as :meth:`url_defaults` but application wide."""
588 self.record_once(
589 lambda s: s.app.url_default_functions.setdefault(None, []).append(f)
590 )
591 return f