blueprints.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  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"], t.Callable]
  14. T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable)
  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: t.Callable | 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 | None = None,
  152. static_url_path: str | None = None,
  153. template_folder: str | os.PathLike | None = None,
  154. url_prefix: str | None = None,
  155. subdomain: str | None = None,
  156. url_defaults: dict | None = None,
  157. root_path: str | None = None,
  158. cli_group: str | None = _sentinel, # type: ignore
  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]] = []
  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: t.Callable) -> 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: t.Callable) -> 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, 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) -> 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(bp_dict, parent_dict):
  318. for key, values in bp_dict.items():
  319. key = name if key is None else f"{name}.{key}"
  320. parent_dict[key].extend(values)
  321. for key, value in self.error_handler_spec.items():
  322. key = name if key is None else f"{name}.{key}"
  323. value = defaultdict(
  324. dict,
  325. {
  326. code: {exc_class: func for exc_class, func in code_values.items()}
  327. for code, code_values in value.items()
  328. },
  329. )
  330. app.error_handler_spec[key] = value
  331. for endpoint, func in self.view_functions.items():
  332. app.view_functions[endpoint] = func
  333. extend(self.before_request_funcs, app.before_request_funcs)
  334. extend(self.after_request_funcs, app.after_request_funcs)
  335. extend(
  336. self.teardown_request_funcs,
  337. app.teardown_request_funcs,
  338. )
  339. extend(self.url_default_functions, app.url_default_functions)
  340. extend(self.url_value_preprocessors, app.url_value_preprocessors)
  341. extend(self.template_context_processors, app.template_context_processors)
  342. @setupmethod
  343. def add_url_rule(
  344. self,
  345. rule: str,
  346. endpoint: str | None = None,
  347. view_func: ft.RouteCallable | None = None,
  348. provide_automatic_options: bool | None = None,
  349. **options: t.Any,
  350. ) -> None:
  351. """Register a URL rule with the blueprint. See :meth:`.Flask.add_url_rule` for
  352. full documentation.
  353. The URL rule is prefixed with the blueprint's URL prefix. The endpoint name,
  354. used with :func:`url_for`, is prefixed with the blueprint's name.
  355. """
  356. if endpoint and "." in endpoint:
  357. raise ValueError("'endpoint' may not contain a dot '.' character.")
  358. if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__:
  359. raise ValueError("'view_func' name may not contain a dot '.' character.")
  360. self.record(
  361. lambda s: s.add_url_rule(
  362. rule,
  363. endpoint,
  364. view_func,
  365. provide_automatic_options=provide_automatic_options,
  366. **options,
  367. )
  368. )
  369. @setupmethod
  370. def app_template_filter(
  371. self, name: str | None = None
  372. ) -> t.Callable[[T_template_filter], T_template_filter]:
  373. """Register a template filter, available in any template rendered by the
  374. application. Equivalent to :meth:`.Flask.template_filter`.
  375. :param name: the optional name of the filter, otherwise the
  376. function name will be used.
  377. """
  378. def decorator(f: T_template_filter) -> T_template_filter:
  379. self.add_app_template_filter(f, name=name)
  380. return f
  381. return decorator
  382. @setupmethod
  383. def add_app_template_filter(
  384. self, f: ft.TemplateFilterCallable, name: str | None = None
  385. ) -> None:
  386. """Register a template filter, available in any template rendered by the
  387. application. Works like the :meth:`app_template_filter` decorator. Equivalent to
  388. :meth:`.Flask.add_template_filter`.
  389. :param name: the optional name of the filter, otherwise the
  390. function name will be used.
  391. """
  392. def register_template(state: BlueprintSetupState) -> None:
  393. state.app.jinja_env.filters[name or f.__name__] = f
  394. self.record_once(register_template)
  395. @setupmethod
  396. def app_template_test(
  397. self, name: str | None = None
  398. ) -> t.Callable[[T_template_test], T_template_test]:
  399. """Register a template test, available in any template rendered by the
  400. application. Equivalent to :meth:`.Flask.template_test`.
  401. .. versionadded:: 0.10
  402. :param name: the optional name of the test, otherwise the
  403. function name will be used.
  404. """
  405. def decorator(f: T_template_test) -> T_template_test:
  406. self.add_app_template_test(f, name=name)
  407. return f
  408. return decorator
  409. @setupmethod
  410. def add_app_template_test(
  411. self, f: ft.TemplateTestCallable, name: str | None = None
  412. ) -> None:
  413. """Register a template test, available in any template rendered by the
  414. application. Works like the :meth:`app_template_test` decorator. Equivalent to
  415. :meth:`.Flask.add_template_test`.
  416. .. versionadded:: 0.10
  417. :param name: the optional name of the test, otherwise the
  418. function name will be used.
  419. """
  420. def register_template(state: BlueprintSetupState) -> None:
  421. state.app.jinja_env.tests[name or f.__name__] = f
  422. self.record_once(register_template)
  423. @setupmethod
  424. def app_template_global(
  425. self, name: str | None = None
  426. ) -> t.Callable[[T_template_global], T_template_global]:
  427. """Register a template global, available in any template rendered by the
  428. application. Equivalent to :meth:`.Flask.template_global`.
  429. .. versionadded:: 0.10
  430. :param name: the optional name of the global, otherwise the
  431. function name will be used.
  432. """
  433. def decorator(f: T_template_global) -> T_template_global:
  434. self.add_app_template_global(f, name=name)
  435. return f
  436. return decorator
  437. @setupmethod
  438. def add_app_template_global(
  439. self, f: ft.TemplateGlobalCallable, name: str | None = None
  440. ) -> None:
  441. """Register a template global, available in any template rendered by the
  442. application. Works like the :meth:`app_template_global` decorator. Equivalent to
  443. :meth:`.Flask.add_template_global`.
  444. .. versionadded:: 0.10
  445. :param name: the optional name of the global, otherwise the
  446. function name will be used.
  447. """
  448. def register_template(state: BlueprintSetupState) -> None:
  449. state.app.jinja_env.globals[name or f.__name__] = f
  450. self.record_once(register_template)
  451. @setupmethod
  452. def before_app_request(self, f: T_before_request) -> T_before_request:
  453. """Like :meth:`before_request`, but before every request, not only those handled
  454. by the blueprint. Equivalent to :meth:`.Flask.before_request`.
  455. """
  456. self.record_once(
  457. lambda s: s.app.before_request_funcs.setdefault(None, []).append(f)
  458. )
  459. return f
  460. @setupmethod
  461. def after_app_request(self, f: T_after_request) -> T_after_request:
  462. """Like :meth:`after_request`, but after every request, not only those handled
  463. by the blueprint. Equivalent to :meth:`.Flask.after_request`.
  464. """
  465. self.record_once(
  466. lambda s: s.app.after_request_funcs.setdefault(None, []).append(f)
  467. )
  468. return f
  469. @setupmethod
  470. def teardown_app_request(self, f: T_teardown) -> T_teardown:
  471. """Like :meth:`teardown_request`, but after every request, not only those
  472. handled by the blueprint. Equivalent to :meth:`.Flask.teardown_request`.
  473. """
  474. self.record_once(
  475. lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f)
  476. )
  477. return f
  478. @setupmethod
  479. def app_context_processor(
  480. self, f: T_template_context_processor
  481. ) -> T_template_context_processor:
  482. """Like :meth:`context_processor`, but for templates rendered by every view, not
  483. only by the blueprint. Equivalent to :meth:`.Flask.context_processor`.
  484. """
  485. self.record_once(
  486. lambda s: s.app.template_context_processors.setdefault(None, []).append(f)
  487. )
  488. return f
  489. @setupmethod
  490. def app_errorhandler(
  491. self, code: type[Exception] | int
  492. ) -> t.Callable[[T_error_handler], T_error_handler]:
  493. """Like :meth:`errorhandler`, but for every request, not only those handled by
  494. the blueprint. Equivalent to :meth:`.Flask.errorhandler`.
  495. """
  496. def decorator(f: T_error_handler) -> T_error_handler:
  497. self.record_once(lambda s: s.app.errorhandler(code)(f))
  498. return f
  499. return decorator
  500. @setupmethod
  501. def app_url_value_preprocessor(
  502. self, f: T_url_value_preprocessor
  503. ) -> T_url_value_preprocessor:
  504. """Like :meth:`url_value_preprocessor`, but for every request, not only those
  505. handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`.
  506. """
  507. self.record_once(
  508. lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f)
  509. )
  510. return f
  511. @setupmethod
  512. def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults:
  513. """Like :meth:`url_defaults`, but for every request, not only those handled by
  514. the blueprint. Equivalent to :meth:`.Flask.url_defaults`.
  515. """
  516. self.record_once(
  517. lambda s: s.app.url_default_functions.setdefault(None, []).append(f)
  518. )
  519. return f