|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- import functools
- from importlib import import_module
- from inspect import getfullargspec
-
- from django.utils.html import conditional_escape
- from django.utils.itercompat import is_iterable
-
- from .base import Node, Template, token_kwargs
- from .exceptions import TemplateSyntaxError
-
-
- class InvalidTemplateLibrary(Exception):
- pass
-
-
- class Library:
- """
- A class for registering template tags and filters. Compiled filter and
- template tag functions are stored in the filters and tags attributes.
- The filter, simple_tag, and inclusion_tag methods provide a convenient
- way to register callables as tags.
- """
- def __init__(self):
- self.filters = {}
- self.tags = {}
-
- def tag(self, name=None, compile_function=None):
- if name is None and compile_function is None:
- # @register.tag()
- return self.tag_function
- elif name is not None and compile_function is None:
- if callable(name):
- # @register.tag
- return self.tag_function(name)
- else:
- # @register.tag('somename') or @register.tag(name='somename')
- def dec(func):
- return self.tag(name, func)
- return dec
- elif name is not None and compile_function is not None:
- # register.tag('somename', somefunc)
- self.tags[name] = compile_function
- return compile_function
- else:
- raise ValueError(
- "Unsupported arguments to Library.tag: (%r, %r)" %
- (name, compile_function),
- )
-
- def tag_function(self, func):
- self.tags[getattr(func, "_decorated_function", func).__name__] = func
- return func
-
- def filter(self, name=None, filter_func=None, **flags):
- """
- Register a callable as a template filter. Example:
-
- @register.filter
- def lower(value):
- return value.lower()
- """
- if name is None and filter_func is None:
- # @register.filter()
- def dec(func):
- return self.filter_function(func, **flags)
- return dec
- elif name is not None and filter_func is None:
- if callable(name):
- # @register.filter
- return self.filter_function(name, **flags)
- else:
- # @register.filter('somename') or @register.filter(name='somename')
- def dec(func):
- return self.filter(name, func, **flags)
- return dec
- elif name is not None and filter_func is not None:
- # register.filter('somename', somefunc)
- self.filters[name] = filter_func
- for attr in ('expects_localtime', 'is_safe', 'needs_autoescape'):
- if attr in flags:
- value = flags[attr]
- # set the flag on the filter for FilterExpression.resolve
- setattr(filter_func, attr, value)
- # set the flag on the innermost decorated function
- # for decorators that need it, e.g. stringfilter
- if hasattr(filter_func, "_decorated_function"):
- setattr(filter_func._decorated_function, attr, value)
- filter_func._filter_name = name
- return filter_func
- else:
- raise ValueError(
- "Unsupported arguments to Library.filter: (%r, %r)" %
- (name, filter_func),
- )
-
- def filter_function(self, func, **flags):
- name = getattr(func, "_decorated_function", func).__name__
- return self.filter(name, func, **flags)
-
- def simple_tag(self, func=None, takes_context=None, name=None):
- """
- Register a callable as a compiled template tag. Example:
-
- @register.simple_tag
- def hello(*args, **kwargs):
- return 'world'
- """
- def dec(func):
- params, varargs, varkw, defaults, kwonly, kwonly_defaults, _ = getfullargspec(func)
- function_name = (name or getattr(func, '_decorated_function', func).__name__)
-
- @functools.wraps(func)
- def compile_func(parser, token):
- bits = token.split_contents()[1:]
- target_var = None
- if len(bits) >= 2 and bits[-2] == 'as':
- target_var = bits[-1]
- bits = bits[:-2]
- args, kwargs = parse_bits(
- parser, bits, params, varargs, varkw, defaults,
- kwonly, kwonly_defaults, takes_context, function_name,
- )
- return SimpleNode(func, takes_context, args, kwargs, target_var)
- self.tag(function_name, compile_func)
- return func
-
- if func is None:
- # @register.simple_tag(...)
- return dec
- elif callable(func):
- # @register.simple_tag
- return dec(func)
- else:
- raise ValueError("Invalid arguments provided to simple_tag")
-
- def inclusion_tag(self, filename, func=None, takes_context=None, name=None):
- """
- Register a callable as an inclusion tag:
-
- @register.inclusion_tag('results.html')
- def show_results(poll):
- choices = poll.choice_set.all()
- return {'choices': choices}
- """
- def dec(func):
- params, varargs, varkw, defaults, kwonly, kwonly_defaults, _ = getfullargspec(func)
- function_name = (name or getattr(func, '_decorated_function', func).__name__)
-
- @functools.wraps(func)
- def compile_func(parser, token):
- bits = token.split_contents()[1:]
- args, kwargs = parse_bits(
- parser, bits, params, varargs, varkw, defaults,
- kwonly, kwonly_defaults, takes_context, function_name,
- )
- return InclusionNode(
- func, takes_context, args, kwargs, filename,
- )
- self.tag(function_name, compile_func)
- return func
- return dec
-
-
- class TagHelperNode(Node):
- """
- Base class for tag helper nodes such as SimpleNode and InclusionNode.
- Manages the positional and keyword arguments to be passed to the decorated
- function.
- """
- def __init__(self, func, takes_context, args, kwargs):
- self.func = func
- self.takes_context = takes_context
- self.args = args
- self.kwargs = kwargs
-
- def get_resolved_arguments(self, context):
- resolved_args = [var.resolve(context) for var in self.args]
- if self.takes_context:
- resolved_args = [context] + resolved_args
- resolved_kwargs = {k: v.resolve(context) for k, v in self.kwargs.items()}
- return resolved_args, resolved_kwargs
-
-
- class SimpleNode(TagHelperNode):
-
- def __init__(self, func, takes_context, args, kwargs, target_var):
- super().__init__(func, takes_context, args, kwargs)
- self.target_var = target_var
-
- def render(self, context):
- resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
- output = self.func(*resolved_args, **resolved_kwargs)
- if self.target_var is not None:
- context[self.target_var] = output
- return ''
- if context.autoescape:
- output = conditional_escape(output)
- return output
-
-
- class InclusionNode(TagHelperNode):
-
- def __init__(self, func, takes_context, args, kwargs, filename):
- super().__init__(func, takes_context, args, kwargs)
- self.filename = filename
-
- def render(self, context):
- """
- Render the specified template and context. Cache the template object
- in render_context to avoid reparsing and loading when used in a for
- loop.
- """
- resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
- _dict = self.func(*resolved_args, **resolved_kwargs)
-
- t = context.render_context.get(self)
- if t is None:
- if isinstance(self.filename, Template):
- t = self.filename
- elif isinstance(getattr(self.filename, 'template', None), Template):
- t = self.filename.template
- elif not isinstance(self.filename, str) and is_iterable(self.filename):
- t = context.template.engine.select_template(self.filename)
- else:
- t = context.template.engine.get_template(self.filename)
- context.render_context[self] = t
- new_context = context.new(_dict)
- # Copy across the CSRF token, if present, because inclusion tags are
- # often used for forms, and we need instructions for using CSRF
- # protection to be as simple as possible.
- csrf_token = context.get('csrf_token')
- if csrf_token is not None:
- new_context['csrf_token'] = csrf_token
- return t.render(new_context)
-
-
- def parse_bits(parser, bits, params, varargs, varkw, defaults,
- kwonly, kwonly_defaults, takes_context, name):
- """
- Parse bits for template tag helpers simple_tag and inclusion_tag, in
- particular by detecting syntax errors and by extracting positional and
- keyword arguments.
- """
- if takes_context:
- if params[0] == 'context':
- params = params[1:]
- else:
- raise TemplateSyntaxError(
- "'%s' is decorated with takes_context=True so it must "
- "have a first argument of 'context'" % name)
- args = []
- kwargs = {}
- unhandled_params = list(params)
- unhandled_kwargs = [
- kwarg for kwarg in kwonly
- if not kwonly_defaults or kwarg not in kwonly_defaults
- ]
- for bit in bits:
- # First we try to extract a potential kwarg from the bit
- kwarg = token_kwargs([bit], parser)
- if kwarg:
- # The kwarg was successfully extracted
- param, value = kwarg.popitem()
- if param not in params and param not in unhandled_kwargs and varkw is None:
- # An unexpected keyword argument was supplied
- raise TemplateSyntaxError(
- "'%s' received unexpected keyword argument '%s'" %
- (name, param))
- elif param in kwargs:
- # The keyword argument has already been supplied once
- raise TemplateSyntaxError(
- "'%s' received multiple values for keyword argument '%s'" %
- (name, param))
- else:
- # All good, record the keyword argument
- kwargs[str(param)] = value
- if param in unhandled_params:
- # If using the keyword syntax for a positional arg, then
- # consume it.
- unhandled_params.remove(param)
- elif param in unhandled_kwargs:
- # Same for keyword-only arguments
- unhandled_kwargs.remove(param)
- else:
- if kwargs:
- raise TemplateSyntaxError(
- "'%s' received some positional argument(s) after some "
- "keyword argument(s)" % name)
- else:
- # Record the positional argument
- args.append(parser.compile_filter(bit))
- try:
- # Consume from the list of expected positional arguments
- unhandled_params.pop(0)
- except IndexError:
- if varargs is None:
- raise TemplateSyntaxError(
- "'%s' received too many positional arguments" %
- name)
- if defaults is not None:
- # Consider the last n params handled, where n is the
- # number of defaults.
- unhandled_params = unhandled_params[:-len(defaults)]
- if unhandled_params or unhandled_kwargs:
- # Some positional arguments were not supplied
- raise TemplateSyntaxError(
- "'%s' did not receive value(s) for the argument(s): %s" %
- (name, ", ".join("'%s'" % p for p in unhandled_params + unhandled_kwargs)))
- return args, kwargs
-
-
- def import_library(name):
- """
- Load a Library object from a template tag module.
- """
- try:
- module = import_module(name)
- except ImportError as e:
- raise InvalidTemplateLibrary(
- "Invalid template library specified. ImportError raised when "
- "trying to load '%s': %s" % (name, e)
- )
- try:
- return module.register
- except AttributeError:
- raise InvalidTemplateLibrary(
- "Module %s does not have a variable named 'register'" % name,
- )
|