1from __future__ import annotations
2
3import typing as t
4
5from jinja2 import BaseLoader
6from jinja2 import Environment as BaseEnvironment
7from jinja2 import Template
8from jinja2 import TemplateNotFound
9
10from .globals import _cv_app
11from .globals import _cv_request
12from .globals import current_app
13from .globals import request
14from .helpers import stream_with_context
15from .signals import before_render_template
16from .signals import template_rendered
17
18if 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
24def _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
39class 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
52class 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
126def _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
138def 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
153def 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
165def _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
188def 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
207def 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)