1from contextlib import contextmanager
2from copy import copy
3
4# Hard-coded processor for easier use of CSRF protection.
5_builtin_context_processors = ("django.template.context_processors.csrf",)
6
7
8class ContextPopException(Exception):
9 "pop() has been called more times than push()"
10 pass
11
12
13class ContextDict(dict):
14 def __init__(self, context, *args, **kwargs):
15 super().__init__(*args, **kwargs)
16
17 context.dicts.append(self)
18 self.context = context
19
20 def __enter__(self):
21 return self
22
23 def __exit__(self, *args, **kwargs):
24 self.context.pop()
25
26
27class BaseContext:
28 def __init__(self, dict_=None):
29 self._reset_dicts(dict_)
30
31 def _reset_dicts(self, value=None):
32 builtins = {"True": True, "False": False, "None": None}
33 self.dicts = [builtins]
34 if isinstance(value, BaseContext):
35 self.dicts += value.dicts[1:]
36 elif value is not None:
37 self.dicts.append(value)
38
39 def __copy__(self):
40 duplicate = BaseContext()
41 duplicate.__class__ = self.__class__
42 duplicate.__dict__ = copy(self.__dict__)
43 duplicate.dicts = self.dicts[:]
44 return duplicate
45
46 def __repr__(self):
47 return repr(self.dicts)
48
49 def __iter__(self):
50 return reversed(self.dicts)
51
52 def push(self, *args, **kwargs):
53 dicts = []
54 for d in args:
55 if isinstance(d, BaseContext):
56 dicts += d.dicts[1:]
57 else:
58 dicts.append(d)
59 return ContextDict(self, *dicts, **kwargs)
60
61 def pop(self):
62 if len(self.dicts) == 1:
63 raise ContextPopException
64 return self.dicts.pop()
65
66 def __setitem__(self, key, value):
67 "Set a variable in the current context"
68 self.dicts[-1][key] = value
69
70 def set_upward(self, key, value):
71 """
72 Set a variable in one of the higher contexts if it exists there,
73 otherwise in the current context.
74 """
75 context = self.dicts[-1]
76 for d in reversed(self.dicts):
77 if key in d:
78 context = d
79 break
80 context[key] = value
81
82 def __getitem__(self, key):
83 "Get a variable's value, starting at the current context and going upward"
84 for d in reversed(self.dicts):
85 if key in d:
86 return d[key]
87 raise KeyError(key)
88
89 def __delitem__(self, key):
90 "Delete a variable from the current context"
91 del self.dicts[-1][key]
92
93 def __contains__(self, key):
94 return any(key in d for d in self.dicts)
95
96 def get(self, key, otherwise=None):
97 for d in reversed(self.dicts):
98 if key in d:
99 return d[key]
100 return otherwise
101
102 def setdefault(self, key, default=None):
103 try:
104 return self[key]
105 except KeyError:
106 self[key] = default
107 return default
108
109 def new(self, values=None):
110 """
111 Return a new context with the same properties, but with only the
112 values given in 'values' stored.
113 """
114 new_context = copy(self)
115 new_context._reset_dicts(values)
116 return new_context
117
118 def flatten(self):
119 """
120 Return self.dicts as one dictionary.
121 """
122 flat = {}
123 for d in self.dicts:
124 flat.update(d)
125 return flat
126
127 def __eq__(self, other):
128 """
129 Compare two contexts by comparing theirs 'dicts' attributes.
130 """
131 if not isinstance(other, BaseContext):
132 return NotImplemented
133 # flatten dictionaries because they can be put in a different order.
134 return self.flatten() == other.flatten()
135
136
137class Context(BaseContext):
138 "A stack container for variable context"
139
140 def __init__(self, dict_=None, autoescape=True, use_l10n=None, use_tz=None):
141 self.autoescape = autoescape
142 self.use_l10n = use_l10n
143 self.use_tz = use_tz
144 self.template_name = "unknown"
145 self.render_context = RenderContext()
146 # Set to the original template -- as opposed to extended or included
147 # templates -- during rendering, see bind_template.
148 self.template = None
149 super().__init__(dict_)
150
151 @contextmanager
152 def bind_template(self, template):
153 if self.template is not None:
154 raise RuntimeError("Context is already bound to a template")
155 self.template = template
156 try:
157 yield
158 finally:
159 self.template = None
160
161 def __copy__(self):
162 duplicate = super().__copy__()
163 duplicate.render_context = copy(self.render_context)
164 return duplicate
165
166 def update(self, other_dict):
167 "Push other_dict to the stack of dictionaries in the Context"
168 if not hasattr(other_dict, "__getitem__"):
169 raise TypeError("other_dict must be a mapping (dictionary-like) object.")
170 if isinstance(other_dict, BaseContext):
171 other_dict = other_dict.dicts[1:].pop()
172 return ContextDict(self, other_dict)
173
174
175class RenderContext(BaseContext):
176 """
177 A stack container for storing Template state.
178
179 RenderContext simplifies the implementation of template Nodes by providing a
180 safe place to store state between invocations of a node's `render` method.
181
182 The RenderContext also provides scoping rules that are more sensible for
183 'template local' variables. The render context stack is pushed before each
184 template is rendered, creating a fresh scope with nothing in it. Name
185 resolution fails if a variable is not found at the top of the RequestContext
186 stack. Thus, variables are local to a specific template and don't affect the
187 rendering of other templates as they would if they were stored in the normal
188 template context.
189 """
190
191 template = None
192
193 def __iter__(self):
194 yield from self.dicts[-1]
195
196 def __contains__(self, key):
197 return key in self.dicts[-1]
198
199 def get(self, key, otherwise=None):
200 return self.dicts[-1].get(key, otherwise)
201
202 def __getitem__(self, key):
203 return self.dicts[-1][key]
204
205 @contextmanager
206 def push_state(self, template, isolated_context=True):
207 initial = self.template
208 self.template = template
209 if isolated_context:
210 self.push()
211 try:
212 yield
213 finally:
214 self.template = initial
215 if isolated_context:
216 self.pop()
217
218
219class RequestContext(Context):
220 """
221 This subclass of template.Context automatically populates itself using
222 the processors defined in the engine's configuration.
223 Additional processors can be specified as a list of callables
224 using the "processors" keyword argument.
225 """
226
227 def __init__(
228 self,
229 request,
230 dict_=None,
231 processors=None,
232 use_l10n=None,
233 use_tz=None,
234 autoescape=True,
235 ):
236 super().__init__(dict_, use_l10n=use_l10n, use_tz=use_tz, autoescape=autoescape)
237 self.request = request
238 self._processors = () if processors is None else tuple(processors)
239 self._processors_index = len(self.dicts)
240
241 # placeholder for context processors output
242 self.update({})
243
244 # empty dict for any new modifications
245 # (so that context processors don't overwrite them)
246 self.update({})
247
248 @contextmanager
249 def bind_template(self, template):
250 if self.template is not None:
251 raise RuntimeError("Context is already bound to a template")
252
253 self.template = template
254 # Set context processors according to the template engine's settings.
255 processors = template.engine.template_context_processors + self._processors
256 updates = {}
257 for processor in processors:
258 context = processor(self.request)
259 try:
260 updates.update(context)
261 except TypeError as e:
262 raise TypeError(
263 f"Context processor {processor.__qualname__} didn't return a "
264 "dictionary."
265 ) from e
266
267 self.dicts[self._processors_index] = updates
268
269 try:
270 yield
271 finally:
272 self.template = None
273 # Unset context processors.
274 self.dicts[self._processors_index] = {}
275
276 def new(self, values=None):
277 new_context = super().new(values)
278 # This is for backwards-compatibility: RequestContexts created via
279 # Context.new don't include values from context processors.
280 if hasattr(new_context, "_processors_index"):
281 del new_context._processors_index
282 return new_context
283
284
285def make_context(context, request=None, **kwargs):
286 """
287 Create a suitable Context from a plain dict and optionally an HttpRequest.
288 """
289 if context is not None and not isinstance(context, dict):
290 raise TypeError(
291 "context must be a dict rather than %s." % context.__class__.__name__
292 )
293 if request is None:
294 context = Context(context, **kwargs)
295 else:
296 # The following pattern is required to ensure values from
297 # context override those from template context processors.
298 original_context = context
299 context = RequestContext(request, **kwargs)
300 if original_context:
301 context.push(original_context)
302 return context