context.py 8.7 KB

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