]>
Commit | Line | Data |
---|---|---|
e0df8241 JR |
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( # type: ignore | |
61 | self, environment: Environment, template: str | |
62 | ) -> tuple[str, str | None, t.Callable | 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: Environment, template: str | |
69 | ) -> tuple[str, str | None, t.Callable | 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: Environment, template: str | |
93 | ) -> tuple[str, str | None, t.Callable | 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( | |
102 | self, template: str | |
103 | ) -> t.Generator[tuple[Scaffold, BaseLoader], None, None]: | |
104 | loader = self.app.jinja_loader | |
105 | if loader is not None: | |
106 | yield self.app, loader | |
107 | ||
108 | for blueprint in self.app.iter_blueprints(): | |
109 | loader = blueprint.jinja_loader | |
110 | if loader is not None: | |
111 | yield blueprint, loader | |
112 | ||
113 | def list_templates(self) -> list[str]: | |
114 | result = set() | |
115 | loader = self.app.jinja_loader | |
116 | if loader is not None: | |
117 | result.update(loader.list_templates()) | |
118 | ||
119 | for blueprint in self.app.iter_blueprints(): | |
120 | loader = blueprint.jinja_loader | |
121 | if loader is not None: | |
122 | for template in loader.list_templates(): | |
123 | result.add(template) | |
124 | ||
125 | return list(result) | |
126 | ||
127 | ||
128 | def _render(app: Flask, template: Template, context: dict[str, t.Any]) -> str: | |
129 | app.update_template_context(context) | |
130 | before_render_template.send( | |
131 | app, _async_wrapper=app.ensure_sync, template=template, context=context | |
132 | ) | |
133 | rv = template.render(context) | |
134 | template_rendered.send( | |
135 | app, _async_wrapper=app.ensure_sync, template=template, context=context | |
136 | ) | |
137 | return rv | |
138 | ||
139 | ||
140 | def render_template( | |
141 | template_name_or_list: str | Template | list[str | Template], | |
142 | **context: t.Any, | |
143 | ) -> str: | |
144 | """Render a template by name with the given context. | |
145 | ||
146 | :param template_name_or_list: The name of the template to render. If | |
147 | a list is given, the first name to exist will be rendered. | |
148 | :param context: The variables to make available in the template. | |
149 | """ | |
150 | app = current_app._get_current_object() # type: ignore[attr-defined] | |
151 | template = app.jinja_env.get_or_select_template(template_name_or_list) | |
152 | return _render(app, template, context) | |
153 | ||
154 | ||
155 | def render_template_string(source: str, **context: t.Any) -> str: | |
156 | """Render a template from the given source string with the given | |
157 | context. | |
158 | ||
159 | :param source: The source code of the template to render. | |
160 | :param context: The variables to make available in the template. | |
161 | """ | |
162 | app = current_app._get_current_object() # type: ignore[attr-defined] | |
163 | template = app.jinja_env.from_string(source) | |
164 | return _render(app, template, context) | |
165 | ||
166 | ||
167 | def _stream( | |
168 | app: Flask, template: Template, context: dict[str, t.Any] | |
169 | ) -> t.Iterator[str]: | |
170 | app.update_template_context(context) | |
171 | before_render_template.send( | |
172 | app, _async_wrapper=app.ensure_sync, template=template, context=context | |
173 | ) | |
174 | ||
175 | def generate() -> t.Iterator[str]: | |
176 | yield from template.generate(context) | |
177 | template_rendered.send( | |
178 | app, _async_wrapper=app.ensure_sync, template=template, context=context | |
179 | ) | |
180 | ||
181 | rv = generate() | |
182 | ||
183 | # If a request context is active, keep it while generating. | |
184 | if request: | |
185 | rv = stream_with_context(rv) | |
186 | ||
187 | return rv | |
188 | ||
189 | ||
190 | def stream_template( | |
191 | template_name_or_list: str | Template | list[str | Template], | |
192 | **context: t.Any, | |
193 | ) -> t.Iterator[str]: | |
194 | """Render a template by name with the given context as a stream. | |
195 | This returns an iterator of strings, which can be used as a | |
196 | streaming response from a view. | |
197 | ||
198 | :param template_name_or_list: The name of the template to render. If | |
199 | a list is given, the first name to exist will be rendered. | |
200 | :param context: The variables to make available in the template. | |
201 | ||
202 | .. versionadded:: 2.2 | |
203 | """ | |
204 | app = current_app._get_current_object() # type: ignore[attr-defined] | |
205 | template = app.jinja_env.get_or_select_template(template_name_or_list) | |
206 | return _stream(app, template, context) | |
207 | ||
208 | ||
209 | def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]: | |
210 | """Render a template from the given source string with the given | |
211 | context as a stream. This returns an iterator of strings, which can | |
212 | be used as a streaming response from a view. | |
213 | ||
214 | :param source: The source code of the template to render. | |
215 | :param context: The variables to make available in the template. | |
216 | ||
217 | .. versionadded:: 2.2 | |
218 | """ | |
219 | app = current_app._get_current_object() # type: ignore[attr-defined] | |
220 | template = app.jinja_env.from_string(source) | |
221 | return _stream(app, template, context) |