You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

library.py 13KB

5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. import functools
  2. from importlib import import_module
  3. from inspect import getfullargspec
  4. from django.utils.html import conditional_escape
  5. from django.utils.itercompat import is_iterable
  6. from .base import Node, Template, token_kwargs
  7. from .exceptions import TemplateSyntaxError
  8. class InvalidTemplateLibrary(Exception):
  9. pass
  10. class Library:
  11. """
  12. A class for registering template tags and filters. Compiled filter and
  13. template tag functions are stored in the filters and tags attributes.
  14. The filter, simple_tag, and inclusion_tag methods provide a convenient
  15. way to register callables as tags.
  16. """
  17. def __init__(self):
  18. self.filters = {}
  19. self.tags = {}
  20. def tag(self, name=None, compile_function=None):
  21. if name is None and compile_function is None:
  22. # @register.tag()
  23. return self.tag_function
  24. elif name is not None and compile_function is None:
  25. if callable(name):
  26. # @register.tag
  27. return self.tag_function(name)
  28. else:
  29. # @register.tag('somename') or @register.tag(name='somename')
  30. def dec(func):
  31. return self.tag(name, func)
  32. return dec
  33. elif name is not None and compile_function is not None:
  34. # register.tag('somename', somefunc)
  35. self.tags[name] = compile_function
  36. return compile_function
  37. else:
  38. raise ValueError(
  39. "Unsupported arguments to Library.tag: (%r, %r)" %
  40. (name, compile_function),
  41. )
  42. def tag_function(self, func):
  43. self.tags[getattr(func, "_decorated_function", func).__name__] = func
  44. return func
  45. def filter(self, name=None, filter_func=None, **flags):
  46. """
  47. Register a callable as a template filter. Example:
  48. @register.filter
  49. def lower(value):
  50. return value.lower()
  51. """
  52. if name is None and filter_func is None:
  53. # @register.filter()
  54. def dec(func):
  55. return self.filter_function(func, **flags)
  56. return dec
  57. elif name is not None and filter_func is None:
  58. if callable(name):
  59. # @register.filter
  60. return self.filter_function(name, **flags)
  61. else:
  62. # @register.filter('somename') or @register.filter(name='somename')
  63. def dec(func):
  64. return self.filter(name, func, **flags)
  65. return dec
  66. elif name is not None and filter_func is not None:
  67. # register.filter('somename', somefunc)
  68. self.filters[name] = filter_func
  69. for attr in ('expects_localtime', 'is_safe', 'needs_autoescape'):
  70. if attr in flags:
  71. value = flags[attr]
  72. # set the flag on the filter for FilterExpression.resolve
  73. setattr(filter_func, attr, value)
  74. # set the flag on the innermost decorated function
  75. # for decorators that need it, e.g. stringfilter
  76. if hasattr(filter_func, "_decorated_function"):
  77. setattr(filter_func._decorated_function, attr, value)
  78. filter_func._filter_name = name
  79. return filter_func
  80. else:
  81. raise ValueError(
  82. "Unsupported arguments to Library.filter: (%r, %r)" %
  83. (name, filter_func),
  84. )
  85. def filter_function(self, func, **flags):
  86. name = getattr(func, "_decorated_function", func).__name__
  87. return self.filter(name, func, **flags)
  88. def simple_tag(self, func=None, takes_context=None, name=None):
  89. """
  90. Register a callable as a compiled template tag. Example:
  91. @register.simple_tag
  92. def hello(*args, **kwargs):
  93. return 'world'
  94. """
  95. def dec(func):
  96. params, varargs, varkw, defaults, kwonly, kwonly_defaults, _ = getfullargspec(func)
  97. function_name = (name or getattr(func, '_decorated_function', func).__name__)
  98. @functools.wraps(func)
  99. def compile_func(parser, token):
  100. bits = token.split_contents()[1:]
  101. target_var = None
  102. if len(bits) >= 2 and bits[-2] == 'as':
  103. target_var = bits[-1]
  104. bits = bits[:-2]
  105. args, kwargs = parse_bits(
  106. parser, bits, params, varargs, varkw, defaults,
  107. kwonly, kwonly_defaults, takes_context, function_name,
  108. )
  109. return SimpleNode(func, takes_context, args, kwargs, target_var)
  110. self.tag(function_name, compile_func)
  111. return func
  112. if func is None:
  113. # @register.simple_tag(...)
  114. return dec
  115. elif callable(func):
  116. # @register.simple_tag
  117. return dec(func)
  118. else:
  119. raise ValueError("Invalid arguments provided to simple_tag")
  120. def inclusion_tag(self, filename, func=None, takes_context=None, name=None):
  121. """
  122. Register a callable as an inclusion tag:
  123. @register.inclusion_tag('results.html')
  124. def show_results(poll):
  125. choices = poll.choice_set.all()
  126. return {'choices': choices}
  127. """
  128. def dec(func):
  129. params, varargs, varkw, defaults, kwonly, kwonly_defaults, _ = getfullargspec(func)
  130. function_name = (name or getattr(func, '_decorated_function', func).__name__)
  131. @functools.wraps(func)
  132. def compile_func(parser, token):
  133. bits = token.split_contents()[1:]
  134. args, kwargs = parse_bits(
  135. parser, bits, params, varargs, varkw, defaults,
  136. kwonly, kwonly_defaults, takes_context, function_name,
  137. )
  138. return InclusionNode(
  139. func, takes_context, args, kwargs, filename,
  140. )
  141. self.tag(function_name, compile_func)
  142. return func
  143. return dec
  144. class TagHelperNode(Node):
  145. """
  146. Base class for tag helper nodes such as SimpleNode and InclusionNode.
  147. Manages the positional and keyword arguments to be passed to the decorated
  148. function.
  149. """
  150. def __init__(self, func, takes_context, args, kwargs):
  151. self.func = func
  152. self.takes_context = takes_context
  153. self.args = args
  154. self.kwargs = kwargs
  155. def get_resolved_arguments(self, context):
  156. resolved_args = [var.resolve(context) for var in self.args]
  157. if self.takes_context:
  158. resolved_args = [context] + resolved_args
  159. resolved_kwargs = {k: v.resolve(context) for k, v in self.kwargs.items()}
  160. return resolved_args, resolved_kwargs
  161. class SimpleNode(TagHelperNode):
  162. def __init__(self, func, takes_context, args, kwargs, target_var):
  163. super().__init__(func, takes_context, args, kwargs)
  164. self.target_var = target_var
  165. def render(self, context):
  166. resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
  167. output = self.func(*resolved_args, **resolved_kwargs)
  168. if self.target_var is not None:
  169. context[self.target_var] = output
  170. return ''
  171. if context.autoescape:
  172. output = conditional_escape(output)
  173. return output
  174. class InclusionNode(TagHelperNode):
  175. def __init__(self, func, takes_context, args, kwargs, filename):
  176. super().__init__(func, takes_context, args, kwargs)
  177. self.filename = filename
  178. def render(self, context):
  179. """
  180. Render the specified template and context. Cache the template object
  181. in render_context to avoid reparsing and loading when used in a for
  182. loop.
  183. """
  184. resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
  185. _dict = self.func(*resolved_args, **resolved_kwargs)
  186. t = context.render_context.get(self)
  187. if t is None:
  188. if isinstance(self.filename, Template):
  189. t = self.filename
  190. elif isinstance(getattr(self.filename, 'template', None), Template):
  191. t = self.filename.template
  192. elif not isinstance(self.filename, str) and is_iterable(self.filename):
  193. t = context.template.engine.select_template(self.filename)
  194. else:
  195. t = context.template.engine.get_template(self.filename)
  196. context.render_context[self] = t
  197. new_context = context.new(_dict)
  198. # Copy across the CSRF token, if present, because inclusion tags are
  199. # often used for forms, and we need instructions for using CSRF
  200. # protection to be as simple as possible.
  201. csrf_token = context.get('csrf_token')
  202. if csrf_token is not None:
  203. new_context['csrf_token'] = csrf_token
  204. return t.render(new_context)
  205. def parse_bits(parser, bits, params, varargs, varkw, defaults,
  206. kwonly, kwonly_defaults, takes_context, name):
  207. """
  208. Parse bits for template tag helpers simple_tag and inclusion_tag, in
  209. particular by detecting syntax errors and by extracting positional and
  210. keyword arguments.
  211. """
  212. if takes_context:
  213. if params[0] == 'context':
  214. params = params[1:]
  215. else:
  216. raise TemplateSyntaxError(
  217. "'%s' is decorated with takes_context=True so it must "
  218. "have a first argument of 'context'" % name)
  219. args = []
  220. kwargs = {}
  221. unhandled_params = list(params)
  222. unhandled_kwargs = [
  223. kwarg for kwarg in kwonly
  224. if not kwonly_defaults or kwarg not in kwonly_defaults
  225. ]
  226. for bit in bits:
  227. # First we try to extract a potential kwarg from the bit
  228. kwarg = token_kwargs([bit], parser)
  229. if kwarg:
  230. # The kwarg was successfully extracted
  231. param, value = kwarg.popitem()
  232. if param not in params and param not in unhandled_kwargs and varkw is None:
  233. # An unexpected keyword argument was supplied
  234. raise TemplateSyntaxError(
  235. "'%s' received unexpected keyword argument '%s'" %
  236. (name, param))
  237. elif param in kwargs:
  238. # The keyword argument has already been supplied once
  239. raise TemplateSyntaxError(
  240. "'%s' received multiple values for keyword argument '%s'" %
  241. (name, param))
  242. else:
  243. # All good, record the keyword argument
  244. kwargs[str(param)] = value
  245. if param in unhandled_params:
  246. # If using the keyword syntax for a positional arg, then
  247. # consume it.
  248. unhandled_params.remove(param)
  249. elif param in unhandled_kwargs:
  250. # Same for keyword-only arguments
  251. unhandled_kwargs.remove(param)
  252. else:
  253. if kwargs:
  254. raise TemplateSyntaxError(
  255. "'%s' received some positional argument(s) after some "
  256. "keyword argument(s)" % name)
  257. else:
  258. # Record the positional argument
  259. args.append(parser.compile_filter(bit))
  260. try:
  261. # Consume from the list of expected positional arguments
  262. unhandled_params.pop(0)
  263. except IndexError:
  264. if varargs is None:
  265. raise TemplateSyntaxError(
  266. "'%s' received too many positional arguments" %
  267. name)
  268. if defaults is not None:
  269. # Consider the last n params handled, where n is the
  270. # number of defaults.
  271. unhandled_params = unhandled_params[:-len(defaults)]
  272. if unhandled_params or unhandled_kwargs:
  273. # Some positional arguments were not supplied
  274. raise TemplateSyntaxError(
  275. "'%s' did not receive value(s) for the argument(s): %s" %
  276. (name, ", ".join("'%s'" % p for p in unhandled_params + unhandled_kwargs)))
  277. return args, kwargs
  278. def import_library(name):
  279. """
  280. Load a Library object from a template tag module.
  281. """
  282. try:
  283. module = import_module(name)
  284. except ImportError as e:
  285. raise InvalidTemplateLibrary(
  286. "Invalid template library specified. ImportError raised when "
  287. "trying to load '%s': %s" % (name, e)
  288. )
  289. try:
  290. return module.register
  291. except AttributeError:
  292. raise InvalidTemplateLibrary(
  293. "Module %s does not have a variable named 'register'" % name,
  294. )