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