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