blueprints.py 24 KB

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