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