1from collections.abc import Iterable
2from functools import wraps
3from importlib import import_module
4from inspect import getfullargspec, unwrap
5
6from django.utils.html import conditional_escape
7
8from .base import Node, Template, token_kwargs
9from .exceptions import TemplateSyntaxError
10
11
12class InvalidTemplateLibrary(Exception):
13 pass
14
15
16class Library:
17 """
18 A class for registering template tags and filters. Compiled filter and
19 template tag functions are stored in the filters and tags attributes.
20 The filter, simple_tag, and inclusion_tag methods provide a convenient
21 way to register callables as tags.
22 """
23
24 def __init__(self):
25 self.filters = {}
26 self.tags = {}
27
28 def tag(self, name=None, compile_function=None):
29 if name is None and compile_function is None:
30 # @register.tag()
31 return self.tag_function
32 elif name is not None and compile_function is None:
33 if callable(name):
34 # @register.tag
35 return self.tag_function(name)
36 else:
37 # @register.tag('somename') or @register.tag(name='somename')
38 def dec(func):
39 return self.tag(name, func)
40
41 return dec
42 elif name is not None and compile_function is not None:
43 # register.tag('somename', somefunc)
44 self.tags[name] = compile_function
45 return compile_function
46 else:
47 raise ValueError(
48 "Unsupported arguments to Library.tag: (%r, %r)"
49 % (name, compile_function),
50 )
51
52 def tag_function(self, func):
53 self.tags[func.__name__] = func
54 return func
55
56 def filter(self, name=None, filter_func=None, **flags):
57 """
58 Register a callable as a template filter. Example:
59
60 @register.filter
61 def lower(value):
62 return value.lower()
63 """
64 if name is None and filter_func is None:
65 # @register.filter()
66 def dec(func):
67 return self.filter_function(func, **flags)
68
69 return dec
70 elif name is not None and filter_func is None:
71 if callable(name):
72 # @register.filter
73 return self.filter_function(name, **flags)
74 else:
75 # @register.filter('somename') or @register.filter(name='somename')
76 def dec(func):
77 return self.filter(name, func, **flags)
78
79 return dec
80 elif name is not None and filter_func is not None:
81 # register.filter('somename', somefunc)
82 self.filters[name] = filter_func
83 for attr in ("expects_localtime", "is_safe", "needs_autoescape"):
84 if attr in flags:
85 value = flags[attr]
86 # set the flag on the filter for FilterExpression.resolve
87 setattr(filter_func, attr, value)
88 # set the flag on the innermost decorated function
89 # for decorators that need it, e.g. stringfilter
90 setattr(unwrap(filter_func), attr, value)
91 filter_func._filter_name = name
92 return filter_func
93 else:
94 raise ValueError(
95 "Unsupported arguments to Library.filter: (%r, %r)"
96 % (name, filter_func),
97 )
98
99 def filter_function(self, func, **flags):
100 return self.filter(func.__name__, func, **flags)
101
102 def simple_tag(self, func=None, takes_context=None, name=None):
103 """
104 Register a callable as a compiled template tag. Example:
105
106 @register.simple_tag
107 def hello(*args, **kwargs):
108 return 'world'
109 """
110
111 def dec(func):
112 (
113 params,
114 varargs,
115 varkw,
116 defaults,
117 kwonly,
118 kwonly_defaults,
119 _,
120 ) = getfullargspec(unwrap(func))
121 function_name = name or func.__name__
122
123 @wraps(func)
124 def compile_func(parser, token):
125 bits = token.split_contents()[1:]
126 target_var = None
127 if len(bits) >= 2 and bits[-2] == "as":
128 target_var = bits[-1]
129 bits = bits[:-2]
130 args, kwargs = parse_bits(
131 parser,
132 bits,
133 params,
134 varargs,
135 varkw,
136 defaults,
137 kwonly,
138 kwonly_defaults,
139 takes_context,
140 function_name,
141 )
142 return SimpleNode(func, takes_context, args, kwargs, target_var)
143
144 self.tag(function_name, compile_func)
145 return func
146
147 if func is None:
148 # @register.simple_tag(...)
149 return dec
150 elif callable(func):
151 # @register.simple_tag
152 return dec(func)
153 else:
154 raise ValueError("Invalid arguments provided to simple_tag")
155
156 def simple_block_tag(self, func=None, takes_context=None, name=None, end_name=None):
157 """
158 Register a callable as a compiled block template tag. Example:
159
160 @register.simple_block_tag
161 def hello(content):
162 return 'world'
163 """
164
165 def dec(func):
166 nonlocal end_name
167
168 (
169 params,
170 varargs,
171 varkw,
172 defaults,
173 kwonly,
174 kwonly_defaults,
175 _,
176 ) = getfullargspec(unwrap(func))
177 function_name = name or func.__name__
178
179 if end_name is None:
180 end_name = f"end{function_name}"
181
182 @wraps(func)
183 def compile_func(parser, token):
184 tag_params = params.copy()
185
186 if takes_context:
187 if len(tag_params) >= 2 and tag_params[1] == "content":
188 del tag_params[1]
189 else:
190 raise TemplateSyntaxError(
191 f"{function_name!r} is decorated with takes_context=True so"
192 " it must have a first argument of 'context' and a second "
193 "argument of 'content'"
194 )
195 elif tag_params and tag_params[0] == "content":
196 del tag_params[0]
197 else:
198 raise TemplateSyntaxError(
199 f"'{function_name}' must have a first argument of 'content'"
200 )
201
202 bits = token.split_contents()[1:]
203 target_var = None
204 if len(bits) >= 2 and bits[-2] == "as":
205 target_var = bits[-1]
206 bits = bits[:-2]
207
208 nodelist = parser.parse((end_name,))
209 parser.delete_first_token()
210
211 args, kwargs = parse_bits(
212 parser,
213 bits,
214 tag_params,
215 varargs,
216 varkw,
217 defaults,
218 kwonly,
219 kwonly_defaults,
220 takes_context,
221 function_name,
222 )
223
224 return SimpleBlockNode(
225 nodelist, func, takes_context, args, kwargs, target_var
226 )
227
228 self.tag(function_name, compile_func)
229 return func
230
231 if func is None:
232 # @register.simple_block_tag(...)
233 return dec
234 elif callable(func):
235 # @register.simple_block_tag
236 return dec(func)
237 else:
238 raise ValueError("Invalid arguments provided to simple_block_tag")
239
240 def inclusion_tag(self, filename, func=None, takes_context=None, name=None):
241 """
242 Register a callable as an inclusion tag:
243
244 @register.inclusion_tag('results.html')
245 def show_results(poll):
246 choices = poll.choice_set.all()
247 return {'choices': choices}
248 """
249
250 def dec(func):
251 (
252 params,
253 varargs,
254 varkw,
255 defaults,
256 kwonly,
257 kwonly_defaults,
258 _,
259 ) = getfullargspec(unwrap(func))
260 function_name = name or func.__name__
261
262 @wraps(func)
263 def compile_func(parser, token):
264 bits = token.split_contents()[1:]
265 args, kwargs = parse_bits(
266 parser,
267 bits,
268 params,
269 varargs,
270 varkw,
271 defaults,
272 kwonly,
273 kwonly_defaults,
274 takes_context,
275 function_name,
276 )
277 return InclusionNode(
278 func,
279 takes_context,
280 args,
281 kwargs,
282 filename,
283 )
284
285 self.tag(function_name, compile_func)
286 return func
287
288 return dec
289
290
291class TagHelperNode(Node):
292 """
293 Base class for tag helper nodes such as SimpleNode and InclusionNode.
294 Manages the positional and keyword arguments to be passed to the decorated
295 function.
296 """
297
298 def __init__(self, func, takes_context, args, kwargs):
299 self.func = func
300 self.takes_context = takes_context
301 self.args = args
302 self.kwargs = kwargs
303
304 def get_resolved_arguments(self, context):
305 resolved_args = [var.resolve(context) for var in self.args]
306 if self.takes_context:
307 resolved_args = [context] + resolved_args
308 resolved_kwargs = {k: v.resolve(context) for k, v in self.kwargs.items()}
309 return resolved_args, resolved_kwargs
310
311
312class SimpleNode(TagHelperNode):
313 child_nodelists = ()
314
315 def __init__(self, func, takes_context, args, kwargs, target_var):
316 super().__init__(func, takes_context, args, kwargs)
317 self.target_var = target_var
318
319 def render(self, context):
320 resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
321 output = self.func(*resolved_args, **resolved_kwargs)
322 if self.target_var is not None:
323 context[self.target_var] = output
324 return ""
325 if context.autoescape:
326 output = conditional_escape(output)
327 return output
328
329
330class SimpleBlockNode(SimpleNode):
331 def __init__(self, nodelist, *args, **kwargs):
332 super().__init__(*args, **kwargs)
333 self.nodelist = nodelist
334
335 def get_resolved_arguments(self, context):
336 resolved_args, resolved_kwargs = super().get_resolved_arguments(context)
337
338 # Restore the "content" argument.
339 # It will move depending on whether takes_context was passed.
340 resolved_args.insert(
341 1 if self.takes_context else 0, self.nodelist.render(context)
342 )
343
344 return resolved_args, resolved_kwargs
345
346
347class InclusionNode(TagHelperNode):
348 def __init__(self, func, takes_context, args, kwargs, filename):
349 super().__init__(func, takes_context, args, kwargs)
350 self.filename = filename
351
352 def render(self, context):
353 """
354 Render the specified template and context. Cache the template object
355 in render_context to avoid reparsing and loading when used in a for
356 loop.
357 """
358 resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
359 _dict = self.func(*resolved_args, **resolved_kwargs)
360
361 t = context.render_context.get(self)
362 if t is None:
363 if isinstance(self.filename, Template):
364 t = self.filename
365 elif isinstance(getattr(self.filename, "template", None), Template):
366 t = self.filename.template
367 elif not isinstance(self.filename, str) and isinstance(
368 self.filename, Iterable
369 ):
370 t = context.template.engine.select_template(self.filename)
371 else:
372 t = context.template.engine.get_template(self.filename)
373 context.render_context[self] = t
374 new_context = context.new(_dict)
375 # Copy across the CSRF token, if present, because inclusion tags are
376 # often used for forms, and we need instructions for using CSRF
377 # protection to be as simple as possible.
378 csrf_token = context.get("csrf_token")
379 if csrf_token is not None:
380 new_context["csrf_token"] = csrf_token
381 return t.render(new_context)
382
383
384def parse_bits(
385 parser,
386 bits,
387 params,
388 varargs,
389 varkw,
390 defaults,
391 kwonly,
392 kwonly_defaults,
393 takes_context,
394 name,
395):
396 """
397 Parse bits for template tag helpers simple_tag and inclusion_tag, in
398 particular by detecting syntax errors and by extracting positional and
399 keyword arguments.
400 """
401 if takes_context:
402 if params and params[0] == "context":
403 params = params[1:]
404 else:
405 raise TemplateSyntaxError(
406 "'%s' is decorated with takes_context=True so it must "
407 "have a first argument of 'context'" % name
408 )
409 args = []
410 kwargs = {}
411 unhandled_params = list(params)
412 unhandled_kwargs = [
413 kwarg for kwarg in kwonly if not kwonly_defaults or kwarg not in kwonly_defaults
414 ]
415 for bit in bits:
416 # First we try to extract a potential kwarg from the bit
417 kwarg = token_kwargs([bit], parser)
418 if kwarg:
419 # The kwarg was successfully extracted
420 param, value = kwarg.popitem()
421 if param not in params and param not in kwonly and varkw is None:
422 # An unexpected keyword argument was supplied
423 raise TemplateSyntaxError(
424 "'%s' received unexpected keyword argument '%s'" % (name, param)
425 )
426 elif param in kwargs:
427 # The keyword argument has already been supplied once
428 raise TemplateSyntaxError(
429 "'%s' received multiple values for keyword argument '%s'"
430 % (name, param)
431 )
432 else:
433 # All good, record the keyword argument
434 kwargs[str(param)] = value
435 if param in unhandled_params:
436 # If using the keyword syntax for a positional arg, then
437 # consume it.
438 unhandled_params.remove(param)
439 elif param in unhandled_kwargs:
440 # Same for keyword-only arguments
441 unhandled_kwargs.remove(param)
442 else:
443 if kwargs:
444 raise TemplateSyntaxError(
445 "'%s' received some positional argument(s) after some "
446 "keyword argument(s)" % name
447 )
448 else:
449 # Record the positional argument
450 args.append(parser.compile_filter(bit))
451 try:
452 # Consume from the list of expected positional arguments
453 unhandled_params.pop(0)
454 except IndexError:
455 if varargs is None:
456 raise TemplateSyntaxError(
457 "'%s' received too many positional arguments" % name
458 )
459 if defaults is not None:
460 # Consider the last n params handled, where n is the
461 # number of defaults.
462 unhandled_params = unhandled_params[: -len(defaults)]
463 if unhandled_params or unhandled_kwargs:
464 # Some positional arguments were not supplied
465 raise TemplateSyntaxError(
466 "'%s' did not receive value(s) for the argument(s): %s"
467 % (name, ", ".join("'%s'" % p for p in unhandled_params + unhandled_kwargs))
468 )
469 return args, kwargs
470
471
472def import_library(name):
473 """
474 Load a Library object from a template tag module.
475 """
476 try:
477 module = import_module(name)
478 except ImportError as e:
479 raise InvalidTemplateLibrary(
480 "Invalid template library specified. ImportError raised when "
481 "trying to load '%s': %s" % (name, e)
482 )
483 try:
484 return module.register
485 except AttributeError:
486 raise InvalidTemplateLibrary(
487 "Module %s does not have a variable named 'register'" % name,
488 )