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