context.py 9.1 KB

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