templating.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. from __future__ import annotations
  2. import typing as t
  3. from jinja2 import BaseLoader
  4. from jinja2 import Environment as BaseEnvironment
  5. from jinja2 import Template
  6. from jinja2 import TemplateNotFound
  7. from .globals import _cv_app
  8. from .globals import _cv_request
  9. from .globals import current_app
  10. from .globals import request
  11. from .helpers import stream_with_context
  12. from .signals import before_render_template
  13. from .signals import template_rendered
  14. if t.TYPE_CHECKING: # pragma: no cover
  15. from .app import Flask
  16. from .sansio.app import App
  17. from .sansio.scaffold import Scaffold
  18. def _default_template_ctx_processor() -> dict[str, t.Any]:
  19. """Default template context processor. Injects `request`,
  20. `session` and `g`.
  21. """
  22. appctx = _cv_app.get(None)
  23. reqctx = _cv_request.get(None)
  24. rv: dict[str, t.Any] = {}
  25. if appctx is not None:
  26. rv["g"] = appctx.g
  27. if reqctx is not None:
  28. rv["request"] = reqctx.request
  29. rv["session"] = reqctx.session
  30. return rv
  31. class Environment(BaseEnvironment):
  32. """Works like a regular Jinja2 environment but has some additional
  33. knowledge of how Flask's blueprint works so that it can prepend the
  34. name of the blueprint to referenced templates if necessary.
  35. """
  36. def __init__(self, app: App, **options: t.Any) -> None:
  37. if "loader" not in options:
  38. options["loader"] = app.create_global_jinja_loader()
  39. BaseEnvironment.__init__(self, **options)
  40. self.app = app
  41. class DispatchingJinjaLoader(BaseLoader):
  42. """A loader that looks for templates in the application and all
  43. the blueprint folders.
  44. """
  45. def __init__(self, app: App) -> None:
  46. self.app = app
  47. def get_source(
  48. self, environment: BaseEnvironment, template: str
  49. ) -> tuple[str, str | None, t.Callable[[], bool] | None]:
  50. if self.app.config["EXPLAIN_TEMPLATE_LOADING"]:
  51. return self._get_source_explained(environment, template)
  52. return self._get_source_fast(environment, template)
  53. def _get_source_explained(
  54. self, environment: BaseEnvironment, template: str
  55. ) -> tuple[str, str | None, t.Callable[[], bool] | None]:
  56. attempts = []
  57. rv: tuple[str, str | None, t.Callable[[], bool] | None] | None
  58. trv: None | (tuple[str, str | None, t.Callable[[], bool] | None]) = None
  59. for srcobj, loader in self._iter_loaders(template):
  60. try:
  61. rv = loader.get_source(environment, template)
  62. if trv is None:
  63. trv = rv
  64. except TemplateNotFound:
  65. rv = None
  66. attempts.append((loader, srcobj, rv))
  67. from .debughelpers import explain_template_loading_attempts
  68. explain_template_loading_attempts(self.app, template, attempts)
  69. if trv is not None:
  70. return trv
  71. raise TemplateNotFound(template)
  72. def _get_source_fast(
  73. self, environment: BaseEnvironment, template: str
  74. ) -> tuple[str, str | None, t.Callable[[], bool] | None]:
  75. for _srcobj, loader in self._iter_loaders(template):
  76. try:
  77. return loader.get_source(environment, template)
  78. except TemplateNotFound:
  79. continue
  80. raise TemplateNotFound(template)
  81. def _iter_loaders(self, template: str) -> t.Iterator[tuple[Scaffold, BaseLoader]]:
  82. loader = self.app.jinja_loader
  83. if loader is not None:
  84. yield self.app, loader
  85. for blueprint in self.app.iter_blueprints():
  86. loader = blueprint.jinja_loader
  87. if loader is not None:
  88. yield blueprint, loader
  89. def list_templates(self) -> list[str]:
  90. result = set()
  91. loader = self.app.jinja_loader
  92. if loader is not None:
  93. result.update(loader.list_templates())
  94. for blueprint in self.app.iter_blueprints():
  95. loader = blueprint.jinja_loader
  96. if loader is not None:
  97. for template in loader.list_templates():
  98. result.add(template)
  99. return list(result)
  100. def _render(app: Flask, template: Template, context: dict[str, t.Any]) -> str:
  101. app.update_template_context(context)
  102. before_render_template.send(
  103. app, _async_wrapper=app.ensure_sync, template=template, context=context
  104. )
  105. rv = template.render(context)
  106. template_rendered.send(
  107. app, _async_wrapper=app.ensure_sync, template=template, context=context
  108. )
  109. return rv
  110. def render_template(
  111. template_name_or_list: str | Template | list[str | Template],
  112. **context: t.Any,
  113. ) -> str:
  114. """Render a template by name with the given context.
  115. :param template_name_or_list: The name of the template to render. If
  116. a list is given, the first name to exist will be rendered.
  117. :param context: The variables to make available in the template.
  118. """
  119. app = current_app._get_current_object() # type: ignore[attr-defined]
  120. template = app.jinja_env.get_or_select_template(template_name_or_list)
  121. return _render(app, template, context)
  122. def render_template_string(source: str, **context: t.Any) -> str:
  123. """Render a template from the given source string with the given
  124. context.
  125. :param source: The source code of the template to render.
  126. :param context: The variables to make available in the template.
  127. """
  128. app = current_app._get_current_object() # type: ignore[attr-defined]
  129. template = app.jinja_env.from_string(source)
  130. return _render(app, template, context)
  131. def _stream(
  132. app: Flask, template: Template, context: dict[str, t.Any]
  133. ) -> t.Iterator[str]:
  134. app.update_template_context(context)
  135. before_render_template.send(
  136. app, _async_wrapper=app.ensure_sync, template=template, context=context
  137. )
  138. def generate() -> t.Iterator[str]:
  139. yield from template.generate(context)
  140. template_rendered.send(
  141. app, _async_wrapper=app.ensure_sync, template=template, context=context
  142. )
  143. rv = generate()
  144. # If a request context is active, keep it while generating.
  145. if request:
  146. rv = stream_with_context(rv)
  147. return rv
  148. def stream_template(
  149. template_name_or_list: str | Template | list[str | Template],
  150. **context: t.Any,
  151. ) -> t.Iterator[str]:
  152. """Render a template by name with the given context as a stream.
  153. This returns an iterator of strings, which can be used as a
  154. streaming response from a view.
  155. :param template_name_or_list: The name of the template to render. If
  156. a list is given, the first name to exist will be rendered.
  157. :param context: The variables to make available in the template.
  158. .. versionadded:: 2.2
  159. """
  160. app = current_app._get_current_object() # type: ignore[attr-defined]
  161. template = app.jinja_env.get_or_select_template(template_name_or_list)
  162. return _stream(app, template, context)
  163. def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]:
  164. """Render a template from the given source string with the given
  165. context as a stream. This returns an iterator of strings, which can
  166. be used as a streaming response from a view.
  167. :param source: The source code of the template to render.
  168. :param context: The variables to make available in the template.
  169. .. versionadded:: 2.2
  170. """
  171. app = current_app._get_current_object() # type: ignore[attr-defined]
  172. template = app.jinja_env.from_string(source)
  173. return _stream(app, template, context)