123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706 |
- import json
- import os
- import typing as t
- from collections import defaultdict
- from functools import update_wrapper
- from . import typing as ft
- from .scaffold import _endpoint_from_view_func
- from .scaffold import _sentinel
- from .scaffold import Scaffold
- from .scaffold import setupmethod
- if t.TYPE_CHECKING: # pragma: no cover
- from .app import Flask
- DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
- T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable)
- T_before_first_request = t.TypeVar(
- "T_before_first_request", bound=ft.BeforeFirstRequestCallable
- )
- T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable)
- T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable)
- T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable)
- T_template_context_processor = t.TypeVar(
- "T_template_context_processor", bound=ft.TemplateContextProcessorCallable
- )
- T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable)
- T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable)
- T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable)
- T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable)
- T_url_value_preprocessor = t.TypeVar(
- "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable
- )
- class BlueprintSetupState:
- """Temporary holder object for registering a blueprint with the
- application. An instance of this class is created by the
- :meth:`~flask.Blueprint.make_setup_state` method and later passed
- to all register callback functions.
- """
- def __init__(
- self,
- blueprint: "Blueprint",
- app: "Flask",
- options: t.Any,
- first_registration: bool,
- ) -> None:
- #: a reference to the current application
- self.app = app
- #: a reference to the blueprint that created this setup state.
- self.blueprint = blueprint
- #: a dictionary with all options that were passed to the
- #: :meth:`~flask.Flask.register_blueprint` method.
- self.options = options
- #: as blueprints can be registered multiple times with the
- #: application and not everything wants to be registered
- #: multiple times on it, this attribute can be used to figure
- #: out if the blueprint was registered in the past already.
- self.first_registration = first_registration
- subdomain = self.options.get("subdomain")
- if subdomain is None:
- subdomain = self.blueprint.subdomain
- #: The subdomain that the blueprint should be active for, ``None``
- #: otherwise.
- self.subdomain = subdomain
- url_prefix = self.options.get("url_prefix")
- if url_prefix is None:
- url_prefix = self.blueprint.url_prefix
- #: The prefix that should be used for all URLs defined on the
- #: blueprint.
- self.url_prefix = url_prefix
- self.name = self.options.get("name", blueprint.name)
- self.name_prefix = self.options.get("name_prefix", "")
- #: A dictionary with URL defaults that is added to each and every
- #: URL that was defined with the blueprint.
- self.url_defaults = dict(self.blueprint.url_values_defaults)
- self.url_defaults.update(self.options.get("url_defaults", ()))
- def add_url_rule(
- self,
- rule: str,
- endpoint: t.Optional[str] = None,
- view_func: t.Optional[t.Callable] = None,
- **options: t.Any,
- ) -> None:
- """A helper method to register a rule (and optionally a view function)
- to the application. The endpoint is automatically prefixed with the
- blueprint's name.
- """
- if self.url_prefix is not None:
- if rule:
- rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/")))
- else:
- rule = self.url_prefix
- options.setdefault("subdomain", self.subdomain)
- if endpoint is None:
- endpoint = _endpoint_from_view_func(view_func) # type: ignore
- defaults = self.url_defaults
- if "defaults" in options:
- defaults = dict(defaults, **options.pop("defaults"))
- self.app.add_url_rule(
- rule,
- f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."),
- view_func,
- defaults=defaults,
- **options,
- )
- class Blueprint(Scaffold):
- """Represents a blueprint, a collection of routes and other
- app-related functions that can be registered on a real application
- later.
- A blueprint is an object that allows defining application functions
- without requiring an application object ahead of time. It uses the
- same decorators as :class:`~flask.Flask`, but defers the need for an
- application by recording them for later registration.
- Decorating a function with a blueprint creates a deferred function
- that is called with :class:`~flask.blueprints.BlueprintSetupState`
- when the blueprint is registered on an application.
- See :doc:`/blueprints` for more information.
- :param name: The name of the blueprint. Will be prepended to each
- endpoint name.
- :param import_name: The name of the blueprint package, usually
- ``__name__``. This helps locate the ``root_path`` for the
- blueprint.
- :param static_folder: A folder with static files that should be
- served by the blueprint's static route. The path is relative to
- the blueprint's root path. Blueprint static files are disabled
- by default.
- :param static_url_path: The url to serve static files from.
- Defaults to ``static_folder``. If the blueprint does not have
- a ``url_prefix``, the app's static route will take precedence,
- and the blueprint's static files won't be accessible.
- :param template_folder: A folder with templates that should be added
- to the app's template search path. The path is relative to the
- blueprint's root path. Blueprint templates are disabled by
- default. Blueprint templates have a lower precedence than those
- in the app's templates folder.
- :param url_prefix: A path to prepend to all of the blueprint's URLs,
- to make them distinct from the rest of the app's routes.
- :param subdomain: A subdomain that blueprint routes will match on by
- default.
- :param url_defaults: A dict of default values that blueprint routes
- will receive by default.
- :param root_path: By default, the blueprint will automatically set
- this based on ``import_name``. In certain situations this
- automatic detection can fail, so the path can be specified
- manually instead.
- .. versionchanged:: 1.1.0
- Blueprints have a ``cli`` group to register nested CLI commands.
- The ``cli_group`` parameter controls the name of the group under
- the ``flask`` command.
- .. versionadded:: 0.7
- """
- _got_registered_once = False
- _json_encoder: t.Union[t.Type[json.JSONEncoder], None] = None
- _json_decoder: t.Union[t.Type[json.JSONDecoder], None] = None
- @property # type: ignore[override]
- def json_encoder( # type: ignore[override]
- self,
- ) -> t.Union[t.Type[json.JSONEncoder], None]:
- """Blueprint-local JSON encoder class to use. Set to ``None`` to use the app's.
- .. deprecated:: 2.2
- Will be removed in Flask 2.3. Customize
- :attr:`json_provider_class` instead.
- .. versionadded:: 0.10
- """
- import warnings
- warnings.warn(
- "'bp.json_encoder' is deprecated and will be removed in Flask 2.3."
- " Customize 'app.json_provider_class' or 'app.json' instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- return self._json_encoder
- @json_encoder.setter
- def json_encoder(self, value: t.Union[t.Type[json.JSONEncoder], None]) -> None:
- import warnings
- warnings.warn(
- "'bp.json_encoder' is deprecated and will be removed in Flask 2.3."
- " Customize 'app.json_provider_class' or 'app.json' instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- self._json_encoder = value
- @property # type: ignore[override]
- def json_decoder( # type: ignore[override]
- self,
- ) -> t.Union[t.Type[json.JSONDecoder], None]:
- """Blueprint-local JSON decoder class to use. Set to ``None`` to use the app's.
- .. deprecated:: 2.2
- Will be removed in Flask 2.3. Customize
- :attr:`json_provider_class` instead.
- .. versionadded:: 0.10
- """
- import warnings
- warnings.warn(
- "'bp.json_decoder' is deprecated and will be removed in Flask 2.3."
- " Customize 'app.json_provider_class' or 'app.json' instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- return self._json_decoder
- @json_decoder.setter
- def json_decoder(self, value: t.Union[t.Type[json.JSONDecoder], None]) -> None:
- import warnings
- warnings.warn(
- "'bp.json_decoder' is deprecated and will be removed in Flask 2.3."
- " Customize 'app.json_provider_class' or 'app.json' instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- self._json_decoder = value
- def __init__(
- self,
- name: str,
- import_name: str,
- static_folder: t.Optional[t.Union[str, os.PathLike]] = None,
- static_url_path: t.Optional[str] = None,
- template_folder: t.Optional[str] = None,
- url_prefix: t.Optional[str] = None,
- subdomain: t.Optional[str] = None,
- url_defaults: t.Optional[dict] = None,
- root_path: t.Optional[str] = None,
- cli_group: t.Optional[str] = _sentinel, # type: ignore
- ):
- super().__init__(
- import_name=import_name,
- static_folder=static_folder,
- static_url_path=static_url_path,
- template_folder=template_folder,
- root_path=root_path,
- )
- if "." in name:
- raise ValueError("'name' may not contain a dot '.' character.")
- self.name = name
- self.url_prefix = url_prefix
- self.subdomain = subdomain
- self.deferred_functions: t.List[DeferredSetupFunction] = []
- if url_defaults is None:
- url_defaults = {}
- self.url_values_defaults = url_defaults
- self.cli_group = cli_group
- self._blueprints: t.List[t.Tuple["Blueprint", dict]] = []
- def _check_setup_finished(self, f_name: str) -> None:
- if self._got_registered_once:
- import warnings
- warnings.warn(
- f"The setup method '{f_name}' can no longer be called on"
- f" the blueprint '{self.name}'. It has already been"
- " registered at least once, any changes will not be"
- " applied consistently.\n"
- "Make sure all imports, decorators, functions, etc."
- " needed to set up the blueprint are done before"
- " registering it.\n"
- "This warning will become an exception in Flask 2.3.",
- UserWarning,
- stacklevel=3,
- )
- @setupmethod
- def record(self, func: t.Callable) -> None:
- """Registers a function that is called when the blueprint is
- registered on the application. This function is called with the
- state as argument as returned by the :meth:`make_setup_state`
- method.
- """
- self.deferred_functions.append(func)
- @setupmethod
- def record_once(self, func: t.Callable) -> None:
- """Works like :meth:`record` but wraps the function in another
- function that will ensure the function is only called once. If the
- blueprint is registered a second time on the application, the
- function passed is not called.
- """
- def wrapper(state: BlueprintSetupState) -> None:
- if state.first_registration:
- func(state)
- self.record(update_wrapper(wrapper, func))
- def make_setup_state(
- self, app: "Flask", options: dict, first_registration: bool = False
- ) -> BlueprintSetupState:
- """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState`
- object that is later passed to the register callback functions.
- Subclasses can override this to return a subclass of the setup state.
- """
- return BlueprintSetupState(self, app, options, first_registration)
- @setupmethod
- def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None:
- """Register a :class:`~flask.Blueprint` on this blueprint. Keyword
- arguments passed to this method will override the defaults set
- on the blueprint.
- .. versionchanged:: 2.0.1
- The ``name`` option can be used to change the (pre-dotted)
- name the blueprint is registered with. This allows the same
- blueprint to be registered multiple times with unique names
- for ``url_for``.
- .. versionadded:: 2.0
- """
- if blueprint is self:
- raise ValueError("Cannot register a blueprint on itself")
- self._blueprints.append((blueprint, options))
- def register(self, app: "Flask", options: dict) -> None:
- """Called by :meth:`Flask.register_blueprint` to register all
- views and callbacks registered on the blueprint with the
- application. Creates a :class:`.BlueprintSetupState` and calls
- each :meth:`record` callback with it.
- :param app: The application this blueprint is being registered
- with.
- :param options: Keyword arguments forwarded from
- :meth:`~Flask.register_blueprint`.
- .. versionchanged:: 2.0.1
- Nested blueprints are registered with their dotted name.
- This allows different blueprints with the same name to be
- nested at different locations.
- .. versionchanged:: 2.0.1
- The ``name`` option can be used to change the (pre-dotted)
- name the blueprint is registered with. This allows the same
- blueprint to be registered multiple times with unique names
- for ``url_for``.
- .. versionchanged:: 2.0.1
- Registering the same blueprint with the same name multiple
- times is deprecated and will become an error in Flask 2.1.
- """
- name_prefix = options.get("name_prefix", "")
- self_name = options.get("name", self.name)
- name = f"{name_prefix}.{self_name}".lstrip(".")
- if name in app.blueprints:
- bp_desc = "this" if app.blueprints[name] is self else "a different"
- existing_at = f" '{name}'" if self_name != name else ""
- raise ValueError(
- f"The name '{self_name}' is already registered for"
- f" {bp_desc} blueprint{existing_at}. Use 'name=' to"
- f" provide a unique name."
- )
- first_bp_registration = not any(bp is self for bp in app.blueprints.values())
- first_name_registration = name not in app.blueprints
- app.blueprints[name] = self
- self._got_registered_once = True
- state = self.make_setup_state(app, options, first_bp_registration)
- if self.has_static_folder:
- state.add_url_rule(
- f"{self.static_url_path}/<path:filename>",
- view_func=self.send_static_file,
- endpoint="static",
- )
- # Merge blueprint data into parent.
- if first_bp_registration or first_name_registration:
- def extend(bp_dict, parent_dict):
- for key, values in bp_dict.items():
- key = name if key is None else f"{name}.{key}"
- parent_dict[key].extend(values)
- for key, value in self.error_handler_spec.items():
- key = name if key is None else f"{name}.{key}"
- value = defaultdict(
- dict,
- {
- code: {
- exc_class: func for exc_class, func in code_values.items()
- }
- for code, code_values in value.items()
- },
- )
- app.error_handler_spec[key] = value
- for endpoint, func in self.view_functions.items():
- app.view_functions[endpoint] = func
- extend(self.before_request_funcs, app.before_request_funcs)
- extend(self.after_request_funcs, app.after_request_funcs)
- extend(
- self.teardown_request_funcs,
- app.teardown_request_funcs,
- )
- extend(self.url_default_functions, app.url_default_functions)
- extend(self.url_value_preprocessors, app.url_value_preprocessors)
- extend(self.template_context_processors, app.template_context_processors)
- for deferred in self.deferred_functions:
- deferred(state)
- cli_resolved_group = options.get("cli_group", self.cli_group)
- if self.cli.commands:
- if cli_resolved_group is None:
- app.cli.commands.update(self.cli.commands)
- elif cli_resolved_group is _sentinel:
- self.cli.name = name
- app.cli.add_command(self.cli)
- else:
- self.cli.name = cli_resolved_group
- app.cli.add_command(self.cli)
- for blueprint, bp_options in self._blueprints:
- bp_options = bp_options.copy()
- bp_url_prefix = bp_options.get("url_prefix")
- if bp_url_prefix is None:
- bp_url_prefix = blueprint.url_prefix
- if state.url_prefix is not None and bp_url_prefix is not None:
- bp_options["url_prefix"] = (
- state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/")
- )
- elif bp_url_prefix is not None:
- bp_options["url_prefix"] = bp_url_prefix
- elif state.url_prefix is not None:
- bp_options["url_prefix"] = state.url_prefix
- bp_options["name_prefix"] = name
- blueprint.register(app, bp_options)
- @setupmethod
- def add_url_rule(
- self,
- rule: str,
- endpoint: t.Optional[str] = None,
- view_func: t.Optional[ft.RouteCallable] = None,
- provide_automatic_options: t.Optional[bool] = None,
- **options: t.Any,
- ) -> None:
- """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for
- the :func:`url_for` function is prefixed with the name of the blueprint.
- """
- if endpoint and "." in endpoint:
- raise ValueError("'endpoint' may not contain a dot '.' character.")
- if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__:
- raise ValueError("'view_func' name may not contain a dot '.' character.")
- self.record(
- lambda s: s.add_url_rule(
- rule,
- endpoint,
- view_func,
- provide_automatic_options=provide_automatic_options,
- **options,
- )
- )
- @setupmethod
- def app_template_filter(
- self, name: t.Optional[str] = None
- ) -> t.Callable[[T_template_filter], T_template_filter]:
- """Register a custom template filter, available application wide. Like
- :meth:`Flask.template_filter` but for a blueprint.
- :param name: the optional name of the filter, otherwise the
- function name will be used.
- """
- def decorator(f: T_template_filter) -> T_template_filter:
- self.add_app_template_filter(f, name=name)
- return f
- return decorator
- @setupmethod
- def add_app_template_filter(
- self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None
- ) -> None:
- """Register a custom template filter, available application wide. Like
- :meth:`Flask.add_template_filter` but for a blueprint. Works exactly
- like the :meth:`app_template_filter` decorator.
- :param name: the optional name of the filter, otherwise the
- function name will be used.
- """
- def register_template(state: BlueprintSetupState) -> None:
- state.app.jinja_env.filters[name or f.__name__] = f
- self.record_once(register_template)
- @setupmethod
- def app_template_test(
- self, name: t.Optional[str] = None
- ) -> t.Callable[[T_template_test], T_template_test]:
- """Register a custom template test, available application wide. Like
- :meth:`Flask.template_test` but for a blueprint.
- .. versionadded:: 0.10
- :param name: the optional name of the test, otherwise the
- function name will be used.
- """
- def decorator(f: T_template_test) -> T_template_test:
- self.add_app_template_test(f, name=name)
- return f
- return decorator
- @setupmethod
- def add_app_template_test(
- self, f: ft.TemplateTestCallable, name: t.Optional[str] = None
- ) -> None:
- """Register a custom template test, available application wide. Like
- :meth:`Flask.add_template_test` but for a blueprint. Works exactly
- like the :meth:`app_template_test` decorator.
- .. versionadded:: 0.10
- :param name: the optional name of the test, otherwise the
- function name will be used.
- """
- def register_template(state: BlueprintSetupState) -> None:
- state.app.jinja_env.tests[name or f.__name__] = f
- self.record_once(register_template)
- @setupmethod
- def app_template_global(
- self, name: t.Optional[str] = None
- ) -> t.Callable[[T_template_global], T_template_global]:
- """Register a custom template global, available application wide. Like
- :meth:`Flask.template_global` but for a blueprint.
- .. versionadded:: 0.10
- :param name: the optional name of the global, otherwise the
- function name will be used.
- """
- def decorator(f: T_template_global) -> T_template_global:
- self.add_app_template_global(f, name=name)
- return f
- return decorator
- @setupmethod
- def add_app_template_global(
- self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None
- ) -> None:
- """Register a custom template global, available application wide. Like
- :meth:`Flask.add_template_global` but for a blueprint. Works exactly
- like the :meth:`app_template_global` decorator.
- .. versionadded:: 0.10
- :param name: the optional name of the global, otherwise the
- function name will be used.
- """
- def register_template(state: BlueprintSetupState) -> None:
- state.app.jinja_env.globals[name or f.__name__] = f
- self.record_once(register_template)
- @setupmethod
- def before_app_request(self, f: T_before_request) -> T_before_request:
- """Like :meth:`Flask.before_request`. Such a function is executed
- before each request, even if outside of a blueprint.
- """
- self.record_once(
- lambda s: s.app.before_request_funcs.setdefault(None, []).append(f)
- )
- return f
- @setupmethod
- def before_app_first_request(
- self, f: T_before_first_request
- ) -> T_before_first_request:
- """Like :meth:`Flask.before_first_request`. Such a function is
- executed before the first request to the application.
- .. deprecated:: 2.2
- Will be removed in Flask 2.3. Run setup code when creating
- the application instead.
- """
- import warnings
- warnings.warn(
- "'before_app_first_request' is deprecated and will be"
- " removed in Flask 2.3. Use 'record_once' instead to run"
- " setup code when registering the blueprint.",
- DeprecationWarning,
- stacklevel=2,
- )
- self.record_once(lambda s: s.app.before_first_request_funcs.append(f))
- return f
- @setupmethod
- def after_app_request(self, f: T_after_request) -> T_after_request:
- """Like :meth:`Flask.after_request` but for a blueprint. Such a function
- is executed after each request, even if outside of the blueprint.
- """
- self.record_once(
- lambda s: s.app.after_request_funcs.setdefault(None, []).append(f)
- )
- return f
- @setupmethod
- def teardown_app_request(self, f: T_teardown) -> T_teardown:
- """Like :meth:`Flask.teardown_request` but for a blueprint. Such a
- function is executed when tearing down each request, even if outside of
- the blueprint.
- """
- self.record_once(
- lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f)
- )
- return f
- @setupmethod
- def app_context_processor(
- self, f: T_template_context_processor
- ) -> T_template_context_processor:
- """Like :meth:`Flask.context_processor` but for a blueprint. Such a
- function is executed each request, even if outside of the blueprint.
- """
- self.record_once(
- lambda s: s.app.template_context_processors.setdefault(None, []).append(f)
- )
- return f
- @setupmethod
- def app_errorhandler(
- self, code: t.Union[t.Type[Exception], int]
- ) -> t.Callable[[T_error_handler], T_error_handler]:
- """Like :meth:`Flask.errorhandler` but for a blueprint. This
- handler is used for all requests, even if outside of the blueprint.
- """
- def decorator(f: T_error_handler) -> T_error_handler:
- self.record_once(lambda s: s.app.errorhandler(code)(f))
- return f
- return decorator
- @setupmethod
- def app_url_value_preprocessor(
- self, f: T_url_value_preprocessor
- ) -> T_url_value_preprocessor:
- """Same as :meth:`url_value_preprocessor` but application wide."""
- self.record_once(
- lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f)
- )
- return f
- @setupmethod
- def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults:
- """Same as :meth:`url_defaults` but application wide."""
- self.record_once(
- lambda s: s.app.url_default_functions.setdefault(None, []).append(f)
- )
- return f
|