Fixed #13956 -- Enabled `*args` and `**kwargs` support for `simple_tag`, `inclusion_tag` and `assignment_tag`. Many thanks to Stephen Burrows for the report and initial patch, to Gregor Müllegger for the initial tests, to SamBull for the suggestions, and to Jannis Leidel for the review and PEP8 cleanup.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16908 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Julien Phalip 2011-09-27 12:15:15 +00:00
parent 29b8e34dfb
commit 8137027fd7
6 changed files with 848 additions and 265 deletions

View File

@ -3,13 +3,16 @@ from functools import partial
from inspect import getargspec
from django.conf import settings
from django.template.context import Context, RequestContext, ContextPopException
from django.template.context import (Context, RequestContext,
ContextPopException)
from django.utils.importlib import import_module
from django.utils.itercompat import is_iterable
from django.utils.text import smart_split, unescape_string_literal, get_text_list
from django.utils.text import (smart_split, unescape_string_literal,
get_text_list)
from django.utils.encoding import smart_unicode, force_unicode, smart_str
from django.utils.translation import ugettext_lazy
from django.utils.safestring import SafeData, EscapeData, mark_safe, mark_for_escaping
from django.utils.safestring import (SafeData, EscapeData, mark_safe,
mark_for_escaping)
from django.utils.formats import localize
from django.utils.html import escape
from django.utils.module_loading import module_has_submodule
@ -19,6 +22,12 @@ TOKEN_TEXT = 0
TOKEN_VAR = 1
TOKEN_BLOCK = 2
TOKEN_COMMENT = 3
TOKEN_MAPPING = {
TOKEN_TEXT: 'Text',
TOKEN_VAR: 'Var',
TOKEN_BLOCK: 'Block',
TOKEN_COMMENT: 'Comment',
}
# template syntax constants
FILTER_SEPARATOR = '|'
@ -34,16 +43,19 @@ TRANSLATOR_COMMENT_MARK = 'Translators'
SINGLE_BRACE_START = '{'
SINGLE_BRACE_END = '}'
ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.'
ALLOWED_VARIABLE_CHARS = ('abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.')
# what to report as the origin for templates that come from non-loader sources
# (e.g. strings)
UNKNOWN_SOURCE = '<unknown source>'
# match a variable or block tag and capture the entire tag, including start/end delimiters
tag_re = re.compile('(%s.*?%s|%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END),
re.escape(COMMENT_TAG_START), re.escape(COMMENT_TAG_END)))
# match a variable or block tag and capture the entire tag, including start/end
# delimiters
tag_re = (re.compile('(%s.*?%s|%s.*?%s|%s.*?%s)' %
(re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END),
re.escape(COMMENT_TAG_START), re.escape(COMMENT_TAG_END))))
# global dictionary of libraries that have been loaded using get_library
libraries = {}
@ -73,7 +85,8 @@ class VariableDoesNotExist(Exception):
return unicode(self).encode('utf-8')
def __unicode__(self):
return self.msg % tuple([force_unicode(p, errors='replace') for p in self.params])
return self.msg % tuple([force_unicode(p, errors='replace')
for p in self.params])
class InvalidTemplateLibrary(Exception):
pass
@ -97,11 +110,13 @@ class StringOrigin(Origin):
return self.source
class Template(object):
def __init__(self, template_string, origin=None, name='<Unknown Template>'):
def __init__(self, template_string, origin=None,
name='<Unknown Template>'):
try:
template_string = smart_unicode(template_string)
except UnicodeDecodeError:
raise TemplateEncodingError("Templates can only be constructed from unicode or UTF-8 strings.")
raise TemplateEncodingError("Templates can only be constructed "
"from unicode or UTF-8 strings.")
if settings.TEMPLATE_DEBUG and origin is None:
origin = StringOrigin(template_string)
self.nodelist = compile_string(template_string, origin)
@ -136,14 +151,15 @@ def compile_string(template_string, origin):
class Token(object):
def __init__(self, token_type, contents):
# token_type must be TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK or TOKEN_COMMENT.
# token_type must be TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK or
# TOKEN_COMMENT.
self.token_type, self.contents = token_type, contents
self.lineno = None
def __str__(self):
return '<%s token: "%s...">' % \
({TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block', TOKEN_COMMENT: 'Comment'}[self.token_type],
self.contents[:20].replace('\n', ''))
token_name = TOKEN_MAPPING[self.token_type]
return ('<%s token: "%s...">' %
(token_name, self.contents[:20].replace('\n', '')))
def split_contents(self):
split = []
@ -167,7 +183,9 @@ class Lexer(object):
self.lineno = 1
def tokenize(self):
"Return a list of tokens from a given template_string."
"""
Return a list of tokens from a given template_string.
"""
in_tag = False
result = []
for bit in tag_re.split(self.template_string):
@ -184,13 +202,21 @@ class Lexer(object):
"""
if in_tag:
if token_string.startswith(VARIABLE_TAG_START):
token = Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip())
token = Token(TOKEN_VAR,
token_string[
len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)
].strip())
elif token_string.startswith(BLOCK_TAG_START):
token = Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip())
token = Token(TOKEN_BLOCK,
token_string[
len(BLOCK_TAG_START):-len(BLOCK_TAG_END)
].strip())
elif token_string.startswith(COMMENT_TAG_START):
content = ''
if token_string.find(TRANSLATOR_COMMENT_MARK):
content = token_string[len(COMMENT_TAG_START):-len(COMMENT_TAG_END)].strip()
content = token_string[
len(COMMENT_TAG_START):-len(COMMENT_TAG_END)
].strip()
token = Token(TOKEN_COMMENT, content)
else:
token = Token(TOKEN_TEXT, token_string)
@ -207,7 +233,8 @@ class Parser(object):
self.add_library(lib)
def parse(self, parse_until=None):
if parse_until is None: parse_until = []
if parse_until is None:
parse_until = []
nodelist = self.create_nodelist()
while self.tokens:
token = self.next_token()
@ -218,17 +245,19 @@ class Parser(object):
self.empty_variable(token)
filter_expression = self.compile_filter(token.contents)
var_node = self.create_variable_node(filter_expression)
self.extend_nodelist(nodelist, var_node,token)
self.extend_nodelist(nodelist, var_node, token)
elif token.token_type == TOKEN_BLOCK:
if token.contents in parse_until:
# put token back on token list so calling code knows why it terminated
# put token back on token list so calling
# code knows why it terminated
self.prepend_token(token)
return nodelist
try:
command = token.contents.split()[0]
except IndexError:
self.empty_block_tag(token)
# execute callback function for this tag and append resulting node
# execute callback function for this tag and append
# resulting node
self.enter_command(command, token)
try:
compile_func = self.tags[command]
@ -264,7 +293,8 @@ class Parser(object):
if nodelist.contains_nontext:
raise AttributeError
except AttributeError:
raise TemplateSyntaxError("%r must be the first tag in the template." % node)
raise TemplateSyntaxError("%r must be the first tag "
"in the template." % node)
if isinstance(nodelist, NodeList) and not isinstance(node, TextNode):
nodelist.contains_nontext = True
nodelist.append(node)
@ -286,11 +316,12 @@ class Parser(object):
def invalid_block_tag(self, token, command, parse_until=None):
if parse_until:
raise self.error(token, "Invalid block tag: '%s', expected %s" % (command, get_text_list(["'%s'" % p for p in parse_until])))
raise self.error(token, "Invalid block tag: '%s', expected %s" %
(command, get_text_list(["'%s'" % p for p in parse_until])))
raise self.error(token, "Invalid block tag: '%s'" % command)
def unclosed_block_tag(self, parse_until):
raise self.error(None, "Unclosed tags: %s " % ', '.join(parse_until))
raise self.error(None, "Unclosed tags: %s " % ', '.join(parse_until))
def compile_function_error(self, token, e):
pass
@ -309,7 +340,9 @@ class Parser(object):
self.filters.update(lib.filters)
def compile_filter(self, token):
"Convenient wrapper for FilterExpression"
"""
Convenient wrapper for FilterExpression
"""
return FilterExpression(token, self)
def find_filter(self, filter_name):
@ -320,8 +353,9 @@ class Parser(object):
class TokenParser(object):
"""
Subclass this and implement the top() method to parse a template line. When
instantiating the parser, pass in the line from the Django template parser.
Subclass this and implement the top() method to parse a template line.
When instantiating the parser, pass in the line from the Django template
parser.
The parser's "tagname" instance-variable stores the name of the tag that
the filter was called with.
@ -333,25 +367,35 @@ class TokenParser(object):
self.tagname = self.tag()
def top(self):
"Overload this method to do the actual parsing and return the result."
"""
Overload this method to do the actual parsing and return the result.
"""
raise NotImplementedError()
def more(self):
"Returns True if there is more stuff in the tag."
"""
Returns True if there is more stuff in the tag.
"""
return self.pointer < len(self.subject)
def back(self):
"Undoes the last microparser. Use this for lookahead and backtracking."
"""
Undoes the last microparser. Use this for lookahead and backtracking.
"""
if not len(self.backout):
raise TemplateSyntaxError("back called without some previous parsing")
raise TemplateSyntaxError("back called without some previous "
"parsing")
self.pointer = self.backout.pop()
def tag(self):
"A microparser that just returns the next tag from the line."
"""
A microparser that just returns the next tag from the line.
"""
subject = self.subject
i = self.pointer
if i >= len(subject):
raise TemplateSyntaxError("expected another tag, found end of string: %s" % subject)
raise TemplateSyntaxError("expected another tag, found "
"end of string: %s" % subject)
p = i
while i < len(subject) and subject[i] not in (' ', '\t'):
i += 1
@ -363,12 +407,18 @@ class TokenParser(object):
return s
def value(self):
"A microparser that parses for a value: some string constant or variable name."
"""
A microparser that parses for a value: some string constant or
variable name.
"""
subject = self.subject
i = self.pointer
def next_space_index(subject, i):
"Increment pointer until a real space (i.e. a space not within quotes) is encountered"
"""
Increment pointer until a real space (i.e. a space not within
quotes) is encountered
"""
while i < len(subject) and subject[i] not in (' ', '\t'):
if subject[i] in ('"', "'"):
c = subject[i]
@ -376,22 +426,29 @@ class TokenParser(object):
while i < len(subject) and subject[i] != c:
i += 1
if i >= len(subject):
raise TemplateSyntaxError("Searching for value. Unexpected end of string in column %d: %s" % (i, subject))
raise TemplateSyntaxError("Searching for value. "
"Unexpected end of string in column %d: %s" %
(i, subject))
i += 1
return i
if i >= len(subject):
raise TemplateSyntaxError("Searching for value. Expected another value but found end of string: %s" % subject)
raise TemplateSyntaxError("Searching for value. Expected another "
"value but found end of string: %s" %
subject)
if subject[i] in ('"', "'"):
p = i
i += 1
while i < len(subject) and subject[i] != subject[p]:
i += 1
if i >= len(subject):
raise TemplateSyntaxError("Searching for value. Unexpected end of string in column %d: %s" % (i, subject))
raise TemplateSyntaxError("Searching for value. Unexpected "
"end of string in column %d: %s" %
(i, subject))
i += 1
# Continue parsing until next "real" space, so that filters are also included
# Continue parsing until next "real" space,
# so that filters are also included
i = next_space_index(subject, i)
res = subject[p:i]
@ -419,10 +476,10 @@ constant_string = r"""
%(strdq)s|
%(strsq)s)
""" % {
'strdq': r'"[^"\\]*(?:\\.[^"\\]*)*"', # double-quoted string
'strsq': r"'[^'\\]*(?:\\.[^'\\]*)*'", # single-quoted string
'i18n_open' : re.escape("_("),
'i18n_close' : re.escape(")"),
'strdq': r'"[^"\\]*(?:\\.[^"\\]*)*"', # double-quoted string
'strsq': r"'[^'\\]*(?:\\.[^'\\]*)*'", # single-quoted string
'i18n_open': re.escape("_("),
'i18n_close': re.escape(")"),
}
constant_string = constant_string.replace("\n", "")
@ -440,18 +497,19 @@ filter_raw_string = r"""
)""" % {
'constant': constant_string,
'num': r'[-+\.]?\d[\d\.e]*',
'var_chars': "\w\." ,
'var_chars': "\w\.",
'filter_sep': re.escape(FILTER_SEPARATOR),
'arg_sep': re.escape(FILTER_ARGUMENT_SEPARATOR),
}
filter_re = re.compile(filter_raw_string, re.UNICODE|re.VERBOSE)
filter_re = re.compile(filter_raw_string, re.UNICODE | re.VERBOSE)
class FilterExpression(object):
r"""
"""
Parses a variable token and its optional filters (all as a single string),
and return a list of tuples of the filter name and arguments.
Sample:
Sample::
>>> token = 'variable|default:"Default value"|date:"Y-m-d"'
>>> p = Parser('')
>>> fe = FilterExpression(token, p)
@ -472,8 +530,10 @@ class FilterExpression(object):
for match in matches:
start = match.start()
if upto != start:
raise TemplateSyntaxError("Could not parse some characters: %s|%s|%s" % \
(token[:upto], token[upto:start], token[start:]))
raise TemplateSyntaxError("Could not parse some characters: "
"%s|%s|%s" %
(token[:upto], token[upto:start],
token[start:]))
if var_obj is None:
var, constant = match.group("var", "constant")
if constant:
@ -482,7 +542,8 @@ class FilterExpression(object):
except VariableDoesNotExist:
var_obj = None
elif var is None:
raise TemplateSyntaxError("Could not find variable at start of %s." % token)
raise TemplateSyntaxError("Could not find variable at "
"start of %s." % token)
else:
var_obj = Variable(var)
else:
@ -498,7 +559,8 @@ class FilterExpression(object):
filters.append((filter_func, args))
upto = match.end()
if upto != len(token):
raise TemplateSyntaxError("Could not parse the remainder: '%s' from '%s'" % (token[upto:], token))
raise TemplateSyntaxError("Could not parse the remainder: '%s' "
"from '%s'" % (token[upto:], token))
self.filters = filters
self.var = var_obj
@ -559,7 +621,8 @@ class FilterExpression(object):
provided.pop(0)
except IndexError:
# Not enough
raise TemplateSyntaxError("%s requires %d arguments, %d provided" % (name, len(nondefs), plen))
raise TemplateSyntaxError("%s requires %d arguments, %d provided" %
(name, len(nondefs), plen))
# Defaults can be overridden.
defaults = defaults and list(defaults) or []
@ -568,7 +631,8 @@ class FilterExpression(object):
defaults.pop(0)
except IndexError:
# Too many.
raise TemplateSyntaxError("%s requires %d arguments, %d provided" % (name, len(nondefs), plen))
raise TemplateSyntaxError("%s requires %d arguments, %d provided" %
(name, len(nondefs), plen))
return True
args_check = staticmethod(args_check)
@ -586,9 +650,9 @@ def resolve_variable(path, context):
return Variable(path).resolve(context)
class Variable(object):
r"""
A template variable, resolvable against a given context. The variable may be
a hard-coded string (if it begins and ends with single or double quote
"""
A template variable, resolvable against a given context. The variable may
be a hard-coded string (if it begins and ends with single or double quote
marks)::
>>> c = {'article': {'section':u'News'}}
@ -642,7 +706,9 @@ class Variable(object):
# Otherwise we'll set self.lookups so that resolve() knows we're
# dealing with a bonafide variable
if var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or var[0] == '_':
raise TemplateSyntaxError("Variables and attributes may not begin with underscores: '%s'" % var)
raise TemplateSyntaxError("Variables and attributes may "
"not begin with underscores: '%s'" %
var)
self.lookups = tuple(var.split(VARIABLE_ATTRIBUTE_SEPARATOR))
def resolve(self, context):
@ -673,22 +739,23 @@ class Variable(object):
instead.
"""
current = context
try: # catch-all for silent variable failures
try: # catch-all for silent variable failures
for bit in self.lookups:
try: # dictionary lookup
try: # dictionary lookup
current = current[bit]
except (TypeError, AttributeError, KeyError):
try: # attribute lookup
try: # attribute lookup
current = getattr(current, bit)
except (TypeError, AttributeError):
try: # list-index lookup
try: # list-index lookup
current = current[int(bit)]
except (IndexError, # list index out of range
ValueError, # invalid literal for int()
KeyError, # current is a dict without `int(bit)` key
TypeError, # unsubscriptable object
):
raise VariableDoesNotExist("Failed lookup for key [%s] in %r", (bit, current)) # missing attribute
except (IndexError, # list index out of range
ValueError, # invalid literal for int()
KeyError, # current is a dict without `int(bit)` key
TypeError): # unsubscriptable object
raise VariableDoesNotExist("Failed lookup for key "
"[%s] in %r",
(bit, current)) # missing attribute
if callable(current):
if getattr(current, 'do_not_call_in_templates', False):
pass
@ -700,7 +767,7 @@ class Variable(object):
except TypeError: # arguments *were* required
# GOTCHA: This will also catch any TypeError
# raised in the function itself.
current = settings.TEMPLATE_STRING_IF_INVALID # invalid method call
current = settings.TEMPLATE_STRING_IF_INVALID # invalid method call
except Exception, e:
if getattr(e, 'silent_variable_failure', False):
current = settings.TEMPLATE_STRING_IF_INVALID
@ -716,14 +783,19 @@ class Node(object):
child_nodelists = ('nodelist',)
def render(self, context):
"Return the node rendered as a string"
"""
Return the node rendered as a string.
"""
pass
def __iter__(self):
yield self
def get_nodes_by_type(self, nodetype):
"Return a list of all nodes (within this node and its nodelist) of the given type"
"""
Return a list of all nodes (within this node and its nodelist)
of the given type
"""
nodes = []
if isinstance(self, nodetype):
nodes.append(self)
@ -776,7 +848,8 @@ def _render_value_in_context(value, context):
"""
value = localize(value, use_l10n=context.use_l10n)
value = force_unicode(value)
if (context.autoescape and not isinstance(value, SafeData)) or isinstance(value, EscapeData):
if ((context.autoescape and not isinstance(value, SafeData)) or
isinstance(value, EscapeData)):
return escape(value)
else:
return value
@ -793,23 +866,159 @@ class VariableNode(Node):
output = self.filter_expression.resolve(context)
except UnicodeDecodeError:
# Unicode conversion can fail sometimes for reasons out of our
# control (e.g. exception rendering). In that case, we fail quietly.
# control (e.g. exception rendering). In that case, we fail
# quietly.
return ''
return _render_value_in_context(output, context)
def generic_tag_compiler(params, defaults, name, node_class, parser, token):
"Returns a template.Node subclass."
bits = token.split_contents()[1:]
bmax = len(params)
def_len = defaults and len(defaults) or 0
bmin = bmax - def_len
if(len(bits) < bmin or len(bits) > bmax):
if bmin == bmax:
message = "%s takes %s arguments" % (name, bmin)
# Regex for token keyword arguments
kwarg_re = re.compile(r"(?:(\w+)=)?(.+)")
def token_kwargs(bits, parser, support_legacy=False):
"""
A utility method for parsing token keyword arguments.
:param bits: A list containing remainder of the token (split by spaces)
that is to be checked for arguments. Valid arguments will be removed
from this list.
:param support_legacy: If set to true ``True``, the legacy format
``1 as foo`` will be accepted. Otherwise, only the standard ``foo=1``
format is allowed.
:returns: A dictionary of the arguments retrieved from the ``bits`` token
list.
There is no requirement for all remaining token ``bits`` to be keyword
arguments, so the dictionary will be returned as soon as an invalid
argument format is reached.
"""
if not bits:
return {}
match = kwarg_re.match(bits[0])
kwarg_format = match and match.group(1)
if not kwarg_format:
if not support_legacy:
return {}
if len(bits) < 3 or bits[1] != 'as':
return {}
kwargs = {}
while bits:
if kwarg_format:
match = kwarg_re.match(bits[0])
if not match or not match.group(1):
return kwargs
key, value = match.groups()
del bits[:1]
else:
message = "%s takes between %s and %s arguments" % (name, bmin, bmax)
raise TemplateSyntaxError(message)
return node_class(bits)
if len(bits) < 3 or bits[1] != 'as':
return kwargs
key, value = bits[2], bits[0]
del bits[:3]
kwargs[key] = parser.compile_filter(value)
if bits and not kwarg_format:
if bits[0] != 'and':
return kwargs
del bits[:1]
return kwargs
def parse_bits(parser, bits, params, varargs, varkw, defaults,
takes_context, name):
"""
Parses bits for template tag helpers (simple_tag, include_tag and
assignment_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)
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.items()[0]
if param not in params 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)
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:
# Some positional arguments were not supplied
raise TemplateSyntaxError(
u"'%s' did not receive value(s) for the argument(s): %s" %
(name, u", ".join([u"'%s'" % p for p in unhandled_params])))
return args, kwargs
def generic_tag_compiler(parser, token, params, varargs, varkw, defaults,
name, takes_context, node_class):
"""
Returns a template.Node subclass.
"""
bits = token.split_contents()[1:]
args, kwargs = parse_bits(parser, bits, params, varargs, varkw,
defaults, takes_context, name)
return node_class(takes_context, args, kwargs)
class TagHelperNode(Node):
"""
Base class for tag helper nodes such as SimpleNode, InclusionNode and
AssignmentNode. Manages the positional and keyword arguments to be passed
to the decorated function.
"""
def __init__(self, takes_context, args, kwargs):
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 = dict((k, v.resolve(context))
for k, v in self.kwargs.items())
return resolved_args, resolved_kwargs
class Library(object):
def __init__(self):
@ -817,10 +1026,10 @@ class Library(object):
self.tags = {}
def tag(self, name=None, compile_function=None):
if name == None and compile_function == None:
if name is None and compile_function is None:
# @register.tag()
return self.tag_function
elif name != None and compile_function == None:
elif name is not None and compile_function is None:
if callable(name):
# @register.tag
return self.tag_function(name)
@ -829,22 +1038,23 @@ class Library(object):
def dec(func):
return self.tag(name, func)
return dec
elif name != None and compile_function != None:
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 InvalidTemplateLibrary("Unsupported arguments to Library.tag: (%r, %r)", (name, compile_function))
raise InvalidTemplateLibrary("Unsupported arguments to "
"Library.tag: (%r, %r)", (name, compile_function))
def tag_function(self,func):
def tag_function(self, func):
self.tags[getattr(func, "_decorated_function", func).__name__] = func
return func
def filter(self, name=None, filter_func=None):
if name == None and filter_func == None:
if name is None and filter_func is None:
# @register.filter()
return self.filter_function
elif filter_func == None:
elif filter_func is None:
if callable(name):
# @register.filter
return self.filter_function(name)
@ -853,12 +1063,13 @@ class Library(object):
def dec(func):
return self.filter(name, func)
return dec
elif name != None and filter_func != None:
elif name is not None and filter_func is not None:
# register.filter('somename', somefunc)
self.filters[name] = filter_func
return filter_func
else:
raise InvalidTemplateLibrary("Unsupported arguments to Library.filter: (%r, %r)", (name, filter_func))
raise InvalidTemplateLibrary("Unsupported arguments to "
"Library.filter: (%r, %r)", (name, filter_func))
def filter_function(self, func):
self.filters[getattr(func, "_decorated_function", func).__name__] = func
@ -866,27 +1077,20 @@ class Library(object):
def simple_tag(self, func=None, takes_context=None, name=None):
def dec(func):
params, xx, xxx, defaults = getargspec(func)
if takes_context:
if params[0] == 'context':
params = params[1:]
else:
raise TemplateSyntaxError("Any tag function decorated with takes_context=True must have a first argument of 'context'")
params, varargs, varkw, defaults = getargspec(func)
class SimpleNode(Node):
def __init__(self, vars_to_resolve):
self.vars_to_resolve = map(Variable, vars_to_resolve)
class SimpleNode(TagHelperNode):
def render(self, context):
resolved_vars = [var.resolve(context) for var in self.vars_to_resolve]
if takes_context:
func_args = [context] + resolved_vars
else:
func_args = resolved_vars
return func(*func_args)
resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
return func(*resolved_args, **resolved_kwargs)
function_name = name or getattr(func, '_decorated_function', func).__name__
compile_func = partial(generic_tag_compiler, params, defaults, function_name, SimpleNode)
function_name = (name or
getattr(func, '_decorated_function', func).__name__)
compile_func = partial(generic_tag_compiler,
params=params, varargs=varargs, varkw=varkw,
defaults=defaults, name=function_name,
takes_context=takes_context, node_class=SimpleNode)
compile_func.__doc__ = func.__doc__
self.tag(function_name, compile_func)
return func
@ -902,52 +1106,33 @@ class Library(object):
def assignment_tag(self, func=None, takes_context=None, name=None):
def dec(func):
params, xx, xxx, defaults = getargspec(func)
if takes_context:
if params[0] == 'context':
params = params[1:]
else:
raise TemplateSyntaxError("Any tag function decorated with takes_context=True must have a first argument of 'context'")
params, varargs, varkw, defaults = getargspec(func)
class AssignmentNode(Node):
def __init__(self, params_vars, target_var):
self.params_vars = map(Variable, params_vars)
class AssignmentNode(TagHelperNode):
def __init__(self, takes_context, args, kwargs, target_var):
super(AssignmentNode, self).__init__(takes_context, args, kwargs)
self.target_var = target_var
def render(self, context):
resolved_vars = [var.resolve(context) for var in self.params_vars]
if takes_context:
func_args = [context] + resolved_vars
else:
func_args = resolved_vars
context[self.target_var] = func(*func_args)
resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
context[self.target_var] = func(*resolved_args, **resolved_kwargs)
return ''
function_name = (name or
getattr(func, '_decorated_function', func).__name__)
def compile_func(parser, token):
bits = token.split_contents()
tag_name = bits[0]
bits = bits[1:]
params_max = len(params)
defaults_length = defaults and len(defaults) or 0
params_min = params_max - defaults_length
if (len(bits) < 2 or bits[-2] != 'as'):
bits = token.split_contents()[1:]
if len(bits) < 2 or bits[-2] != 'as':
raise TemplateSyntaxError(
"'%s' tag takes at least 2 arguments and the "
"second last argument must be 'as'" % tag_name)
params_vars = bits[:-2]
"second last argument must be 'as'" % function_name)
target_var = bits[-1]
if (len(params_vars) < params_min or
len(params_vars) > params_max):
if params_min == params_max:
raise TemplateSyntaxError(
"%s takes %s arguments" % (tag_name, params_min))
else:
raise TemplateSyntaxError(
"%s takes between %s and %s arguments"
% (tag_name, params_min, params_max))
return AssignmentNode(params_vars, target_var)
bits = bits[:-2]
args, kwargs = parse_bits(parser, bits, params,
varargs, varkw, defaults, takes_context, function_name)
return AssignmentNode(takes_context, args, kwargs, target_var)
function_name = name or getattr(func, '_decorated_function', func).__name__
compile_func.__doc__ = func.__doc__
self.tag(function_name, compile_func)
return func
@ -963,25 +1148,13 @@ class Library(object):
def inclusion_tag(self, file_name, context_class=Context, takes_context=False, name=None):
def dec(func):
params, xx, xxx, defaults = getargspec(func)
if takes_context:
if params[0] == 'context':
params = params[1:]
else:
raise TemplateSyntaxError("Any tag function decorated with takes_context=True must have a first argument of 'context'")
params, varargs, varkw, defaults = getargspec(func)
class InclusionNode(Node):
def __init__(self, vars_to_resolve):
self.vars_to_resolve = map(Variable, vars_to_resolve)
class InclusionNode(TagHelperNode):
def render(self, context):
resolved_vars = [var.resolve(context) for var in self.vars_to_resolve]
if takes_context:
args = [context] + resolved_vars
else:
args = resolved_vars
dict = func(*args)
resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
_dict = func(*resolved_args, **resolved_kwargs)
if not getattr(self, 'nodelist', False):
from django.template.loader import get_template, select_template
@ -992,62 +1165,73 @@ class Library(object):
else:
t = get_template(file_name)
self.nodelist = t.nodelist
new_context = context_class(dict, **{
new_context = context_class(_dict, **{
'autoescape': context.autoescape,
'current_app': context.current_app,
'use_l10n': context.use_l10n,
})
# 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.
# 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', None)
if csrf_token is not None:
new_context['csrf_token'] = csrf_token
return self.nodelist.render(new_context)
function_name = name or getattr(func, '_decorated_function', func).__name__
compile_func = partial(generic_tag_compiler, params, defaults, function_name, InclusionNode)
function_name = (name or
getattr(func, '_decorated_function', func).__name__)
compile_func = partial(generic_tag_compiler,
params=params, varargs=varargs, varkw=varkw,
defaults=defaults, name=function_name,
takes_context=takes_context, node_class=InclusionNode)
compile_func.__doc__ = func.__doc__
self.tag(function_name, compile_func)
return func
return dec
def import_library(taglib_module):
"""Load a template tag library module.
"""
Load a template tag library module.
Verifies that the library contains a 'register' attribute, and
returns that attribute as the representation of the library
"""
app_path, taglib = taglib_module.rsplit('.',1)
app_path, taglib = taglib_module.rsplit('.', 1)
app_module = import_module(app_path)
try:
mod = import_module(taglib_module)
except ImportError, e:
# If the ImportError is because the taglib submodule does not exist, that's not
# an error that should be raised. If the submodule exists and raised an ImportError
# on the attempt to load it, that we want to raise.
# If the ImportError is because the taglib submodule does not exist,
# that's not an error that should be raised. If the submodule exists
# and raised an ImportError on the attempt to load it, that we want
# to raise.
if not module_has_submodule(app_module, taglib):
return None
else:
raise InvalidTemplateLibrary("ImportError raised loading %s: %s" % (taglib_module, e))
raise InvalidTemplateLibrary("ImportError raised loading %s: %s" %
(taglib_module, e))
try:
return mod.register
except AttributeError:
raise InvalidTemplateLibrary("Template library %s does not have a variable named 'register'" % taglib_module)
raise InvalidTemplateLibrary("Template library %s does not have "
"a variable named 'register'" %
taglib_module)
templatetags_modules = []
def get_templatetags_modules():
"""Return the list of all available template tag modules.
"""
Return the list of all available template tag modules.
Caches the result for faster access.
"""
global templatetags_modules
if not templatetags_modules:
_templatetags_modules = []
# Populate list once per process. Mutate the local list first, and then
# assign it to the global name to ensure there are no cases where two
# threads try to populate it simultaneously.
# Populate list once per process. Mutate the local list first, and
# then assign it to the global name to ensure there are no cases where
# two threads try to populate it simultaneously.
for app_module in ['django'] + list(settings.INSTALLED_APPS):
try:
templatetag_module = '%s.templatetags' % app_module
@ -1062,12 +1246,13 @@ def get_library(library_name):
"""
Load the template library module with the given name.
If library is not already loaded loop over all templatetags modules to locate it.
If library is not already loaded loop over all templatetags modules
to locate it.
{% load somelib %} and {% load someotherlib %} loops twice.
Subsequent loads eg. {% load somelib %} in the same process will grab the cached
module from libraries.
Subsequent loads eg. {% load somelib %} in the same process will grab
the cached module from libraries.
"""
lib = libraries.get(library_name, None)
if not lib:
@ -1081,11 +1266,16 @@ def get_library(library_name):
libraries[library_name] = lib
break
if not lib:
raise InvalidTemplateLibrary("Template library %s not found, tried %s" % (library_name, ','.join(tried_modules)))
raise InvalidTemplateLibrary("Template library %s not found, "
"tried %s" %
(library_name,
','.join(tried_modules)))
return lib
def add_to_builtins(module):
builtins.append(import_library(module))
add_to_builtins('django.template.defaulttags')
add_to_builtins('django.template.defaultfilters')

View File

@ -10,64 +10,13 @@ from django.template.base import (Node, NodeList, Template, Library,
TemplateSyntaxError, VariableDoesNotExist, InvalidTemplateLibrary,
BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END,
SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END,
get_library)
get_library, token_kwargs, kwarg_re)
from django.template.smartif import IfParser, Literal
from django.template.defaultfilters import date
from django.utils.encoding import smart_str, smart_unicode
from django.utils.safestring import mark_safe
register = Library()
# Regex for token keyword arguments
kwarg_re = re.compile(r"(?:(\w+)=)?(.+)")
def token_kwargs(bits, parser, support_legacy=False):
"""
A utility method for parsing token keyword arguments.
:param bits: A list containing remainder of the token (split by spaces)
that is to be checked for arguments. Valid arguments will be removed
from this list.
:param support_legacy: If set to true ``True``, the legacy format
``1 as foo`` will be accepted. Otherwise, only the standard ``foo=1``
format is allowed.
:returns: A dictionary of the arguments retrieved from the ``bits`` token
list.
There is no requirement for all remaining token ``bits`` to be keyword
arguments, so the dictionary will be returned as soon as an invalid
argument format is reached.
"""
if not bits:
return {}
match = kwarg_re.match(bits[0])
kwarg_format = match and match.group(1)
if not kwarg_format:
if not support_legacy:
return {}
if len(bits) < 3 or bits[1] != 'as':
return {}
kwargs = {}
while bits:
if kwarg_format:
match = kwarg_re.match(bits[0])
if not match or not match.group(1):
return kwargs
key, value = match.groups()
del bits[:1]
else:
if len(bits) < 3 or bits[1] != 'as':
return kwargs
key, value = bits[2], bits[0]
del bits[:3]
kwargs[key] = parser.compile_filter(value)
if bits and not kwarg_format:
if bits[0] != 'and':
return kwargs
del bits[:1]
return kwargs
class AutoEscapeControlNode(Node):
"""Implements the actions of the autoescape tag."""

View File

@ -698,6 +698,29 @@ If you need to rename your tag, you can provide a custom name for it::
def some_function(value):
return value - 1
.. versionadded:: 1.4
``simple_tag`` functions may accept any number of positional or keyword
arguments. For example:
.. code-block:: python
@register.simple_tag
def my_tag(a, b, *args, **kwargs):
warning = kwargs['warning']
profile = kwargs['profile']
...
return ...
Then in the template any number of arguments, separated by spaces, may be
passed to the template tag. Like in Python, the values for keyword arguments
are set using the equal sign ("``=``") and must be provided after the positional
arguments. For example:
.. code-block:: html+django
{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}
.. _howto-custom-template-tags-assignment-tags:
Assignment tags
@ -761,6 +784,27 @@ Or, using decorator syntax:
For more information on how the ``takes_context`` option works, see the section
on :ref:`inclusion tags<howto-custom-template-tags-inclusion-tags>`.
``assignment_tag`` functions may accept any number of positional or keyword
arguments. For example:
.. code-block:: python
@register.assignment_tag
def my_tag(a, b, *args, **kwargs):
warning = kwargs['warning']
profile = kwargs['profile']
...
return ...
Then in the template any number of arguments, separated by spaces, may be
passed to the template tag. Like in Python, the values for keyword arguments
are set using the equal sign ("``=``") and must be provided after the positional
arguments. For example:
.. code-block:: html+django
{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile as the_result %}
.. _howto-custom-template-tags-inclusion-tags:
Inclusion tags
@ -884,6 +928,29 @@ The ``takes_context`` parameter defaults to ``False``. When it's set to *True*,
the tag is passed the context object, as in this example. That's the only
difference between this case and the previous ``inclusion_tag`` example.
.. versionadded:: 1.4
``inclusion_tag`` functions may accept any number of positional or keyword
arguments. For example:
.. code-block:: python
@register.inclusion_tag('my_template.html')
def my_tag(a, b, *args, **kwargs):
warning = kwargs['warning']
profile = kwargs['profile']
...
return ...
Then in the template any number of arguments, separated by spaces, may be
passed to the template tag. Like in Python, the values for keyword arguments
are set using the equal sign ("``=``") and must be provided after the positional
arguments. For example:
.. code-block:: html+django
{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}
Setting a variable in the context
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -162,6 +162,31 @@ A new helper function,
``template.Library`` to ease the creation of template tags that store some
data in a specified context variable.
``*args`` and ``**kwargs`` support for template tag helper functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:ref:`simple_tag<howto-custom-template-tags-simple-tags>`, :ref:`inclusion_tag
<howto-custom-template-tags-inclusion-tags>` and the newly introduced
:ref:`assignment_tag<howto-custom-template-tags-assignment-tags>` template
helper functions may now accept any number of positional or keyword arguments.
For example:
.. code-block:: python
@register.simple_tag
def my_tag(a, b, *args, **kwargs):
warning = kwargs['warning']
profile = kwargs['profile']
...
return ...
Then in the template any number of arguments may be passed to the template tag.
For example:
.. code-block:: html+django
{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}
``truncatechars`` template filter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -35,6 +35,56 @@ class CustomTagTests(TestCase):
t = template.Template('{% load custom %}{% params_and_context 37 %}')
self.assertEqual(t.render(c), u'params_and_context - Expected result (context value: 42): 37')
t = template.Template('{% load custom %}{% simple_two_params 37 42 %}')
self.assertEqual(t.render(c), u'simple_two_params - Expected result: 37, 42')
t = template.Template('{% load custom %}{% simple_one_default 37 %}')
self.assertEqual(t.render(c), u'simple_one_default - Expected result: 37, hi')
t = template.Template('{% load custom %}{% simple_one_default 37 two="hello" %}')
self.assertEqual(t.render(c), u'simple_one_default - Expected result: 37, hello')
t = template.Template('{% load custom %}{% simple_one_default one=99 two="hello" %}')
self.assertEqual(t.render(c), u'simple_one_default - Expected result: 99, hello')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'simple_one_default' received unexpected keyword argument 'three'",
template.Template, '{% load custom %}{% simple_one_default 99 two="hello" three="foo" %}')
t = template.Template('{% load custom %}{% simple_one_default 37 42 %}')
self.assertEqual(t.render(c), u'simple_one_default - Expected result: 37, 42')
t = template.Template('{% load custom %}{% simple_unlimited_args 37 %}')
self.assertEqual(t.render(c), u'simple_unlimited_args - Expected result: 37, hi')
t = template.Template('{% load custom %}{% simple_unlimited_args 37 42 56 89 %}')
self.assertEqual(t.render(c), u'simple_unlimited_args - Expected result: 37, 42, 56, 89')
t = template.Template('{% load custom %}{% simple_only_unlimited_args %}')
self.assertEqual(t.render(c), u'simple_only_unlimited_args - Expected result: ')
t = template.Template('{% load custom %}{% simple_only_unlimited_args 37 42 56 89 %}')
self.assertEqual(t.render(c), u'simple_only_unlimited_args - Expected result: 37, 42, 56, 89')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'simple_two_params' received too many positional arguments",
template.Template, '{% load custom %}{% simple_two_params 37 42 56 %}')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'simple_one_default' received too many positional arguments",
template.Template, '{% load custom %}{% simple_one_default 37 42 56 %}')
t = template.Template('{% load custom %}{% simple_unlimited_args_kwargs 37 40|add:2 56 eggs="scrambled" four=1|add:3 %}')
self.assertEqual(t.render(c), u'simple_unlimited_args_kwargs - Expected result: 37, 42, 56 / eggs=scrambled, four=4')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'simple_unlimited_args_kwargs' received some positional argument\(s\) after some keyword argument\(s\)",
template.Template, '{% load custom %}{% simple_unlimited_args_kwargs 37 40|add:2 eggs="scrambled" 56 four=1|add:3 %}')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'simple_unlimited_args_kwargs' received multiple values for keyword argument 'eggs'",
template.Template, '{% load custom %}{% simple_unlimited_args_kwargs 37 eggs="scrambled" eggs="scrambled" %}')
def test_simple_tag_registration(self):
# Test that the decorators preserve the decorated function's docstring, name and attributes.
self.verify_tag(custom.no_params, 'no_params')
@ -42,16 +92,14 @@ class CustomTagTests(TestCase):
self.verify_tag(custom.explicit_no_context, 'explicit_no_context')
self.verify_tag(custom.no_params_with_context, 'no_params_with_context')
self.verify_tag(custom.params_and_context, 'params_and_context')
self.verify_tag(custom.simple_unlimited_args_kwargs, 'simple_unlimited_args_kwargs')
self.verify_tag(custom.simple_tag_without_context_parameter, 'simple_tag_without_context_parameter')
def test_simple_tag_missing_context(self):
# That the 'context' parameter must be present when takes_context is True
def a_simple_tag_without_parameters(arg):
"""Expected __doc__"""
return "Expected result"
register = template.Library()
decorator = register.simple_tag(takes_context=True)
self.assertRaises(template.TemplateSyntaxError, decorator, a_simple_tag_without_parameters)
# The 'context' parameter must be present when takes_context is True
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'simple_tag_without_context_parameter' is decorated with takes_context=True so it must have a first argument of 'context'",
template.Template, '{% load custom %}{% simple_tag_without_context_parameter 123 %}')
def test_inclusion_tags(self):
c = template.Context({'value': 42})
@ -71,6 +119,70 @@ class CustomTagTests(TestCase):
t = template.Template('{% load custom %}{% inclusion_params_and_context 37 %}')
self.assertEqual(t.render(c), u'inclusion_params_and_context - Expected result (context value: 42): 37\n')
t = template.Template('{% load custom %}{% inclusion_two_params 37 42 %}')
self.assertEqual(t.render(c), u'inclusion_two_params - Expected result: 37, 42\n')
t = template.Template('{% load custom %}{% inclusion_one_default 37 %}')
self.assertEqual(t.render(c), u'inclusion_one_default - Expected result: 37, hi\n')
t = template.Template('{% load custom %}{% inclusion_one_default 37 two="hello" %}')
self.assertEqual(t.render(c), u'inclusion_one_default - Expected result: 37, hello\n')
t = template.Template('{% load custom %}{% inclusion_one_default one=99 two="hello" %}')
self.assertEqual(t.render(c), u'inclusion_one_default - Expected result: 99, hello\n')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'inclusion_one_default' received unexpected keyword argument 'three'",
template.Template, '{% load custom %}{% inclusion_one_default 99 two="hello" three="foo" %}')
t = template.Template('{% load custom %}{% inclusion_one_default 37 42 %}')
self.assertEqual(t.render(c), u'inclusion_one_default - Expected result: 37, 42\n')
t = template.Template('{% load custom %}{% inclusion_unlimited_args 37 %}')
self.assertEqual(t.render(c), u'inclusion_unlimited_args - Expected result: 37, hi\n')
t = template.Template('{% load custom %}{% inclusion_unlimited_args 37 42 56 89 %}')
self.assertEqual(t.render(c), u'inclusion_unlimited_args - Expected result: 37, 42, 56, 89\n')
t = template.Template('{% load custom %}{% inclusion_only_unlimited_args %}')
self.assertEqual(t.render(c), u'inclusion_only_unlimited_args - Expected result: \n')
t = template.Template('{% load custom %}{% inclusion_only_unlimited_args 37 42 56 89 %}')
self.assertEqual(t.render(c), u'inclusion_only_unlimited_args - Expected result: 37, 42, 56, 89\n')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'inclusion_two_params' received too many positional arguments",
template.Template, '{% load custom %}{% inclusion_two_params 37 42 56 %}')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'inclusion_one_default' received too many positional arguments",
template.Template, '{% load custom %}{% inclusion_one_default 37 42 56 %}')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'inclusion_one_default' did not receive value\(s\) for the argument\(s\): 'one'",
template.Template, '{% load custom %}{% inclusion_one_default %}')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'inclusion_unlimited_args' did not receive value\(s\) for the argument\(s\): 'one'",
template.Template, '{% load custom %}{% inclusion_unlimited_args %}')
t = template.Template('{% load custom %}{% inclusion_unlimited_args_kwargs 37 40|add:2 56 eggs="scrambled" four=1|add:3 %}')
self.assertEqual(t.render(c), u'inclusion_unlimited_args_kwargs - Expected result: 37, 42, 56 / eggs=scrambled, four=4\n')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'inclusion_unlimited_args_kwargs' received some positional argument\(s\) after some keyword argument\(s\)",
template.Template, '{% load custom %}{% inclusion_unlimited_args_kwargs 37 40|add:2 eggs="scrambled" 56 four=1|add:3 %}')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'inclusion_unlimited_args_kwargs' received multiple values for keyword argument 'eggs'",
template.Template, '{% load custom %}{% inclusion_unlimited_args_kwargs 37 eggs="scrambled" eggs="scrambled" %}')
def test_include_tag_missing_context(self):
# The 'context' parameter must be present when takes_context is True
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'inclusion_tag_without_context_parameter' is decorated with takes_context=True so it must have a first argument of 'context'",
template.Template, '{% load custom %}{% inclusion_tag_without_context_parameter 123 %}')
def test_inclusion_tags_from_template(self):
c = template.Context({'value': 42})
@ -89,6 +201,27 @@ class CustomTagTests(TestCase):
t = template.Template('{% load custom %}{% inclusion_params_and_context_from_template 37 %}')
self.assertEqual(t.render(c), u'inclusion_params_and_context_from_template - Expected result (context value: 42): 37\n')
t = template.Template('{% load custom %}{% inclusion_two_params_from_template 37 42 %}')
self.assertEqual(t.render(c), u'inclusion_two_params_from_template - Expected result: 37, 42\n')
t = template.Template('{% load custom %}{% inclusion_one_default_from_template 37 %}')
self.assertEqual(t.render(c), u'inclusion_one_default_from_template - Expected result: 37, hi\n')
t = template.Template('{% load custom %}{% inclusion_one_default_from_template 37 42 %}')
self.assertEqual(t.render(c), u'inclusion_one_default_from_template - Expected result: 37, 42\n')
t = template.Template('{% load custom %}{% inclusion_unlimited_args_from_template 37 %}')
self.assertEqual(t.render(c), u'inclusion_unlimited_args_from_template - Expected result: 37, hi\n')
t = template.Template('{% load custom %}{% inclusion_unlimited_args_from_template 37 42 56 89 %}')
self.assertEqual(t.render(c), u'inclusion_unlimited_args_from_template - Expected result: 37, 42, 56, 89\n')
t = template.Template('{% load custom %}{% inclusion_only_unlimited_args_from_template %}')
self.assertEqual(t.render(c), u'inclusion_only_unlimited_args_from_template - Expected result: \n')
t = template.Template('{% load custom %}{% inclusion_only_unlimited_args_from_template 37 42 56 89 %}')
self.assertEqual(t.render(c), u'inclusion_only_unlimited_args_from_template - Expected result: 37, 42, 56, 89\n')
def test_inclusion_tag_registration(self):
# Test that the decorators preserve the decorated function's docstring, name and attributes.
self.verify_tag(custom.inclusion_no_params, 'inclusion_no_params')
@ -96,6 +229,14 @@ class CustomTagTests(TestCase):
self.verify_tag(custom.inclusion_explicit_no_context, 'inclusion_explicit_no_context')
self.verify_tag(custom.inclusion_no_params_with_context, 'inclusion_no_params_with_context')
self.verify_tag(custom.inclusion_params_and_context, 'inclusion_params_and_context')
self.verify_tag(custom.inclusion_two_params, 'inclusion_two_params')
self.verify_tag(custom.inclusion_one_default, 'inclusion_one_default')
self.verify_tag(custom.inclusion_unlimited_args, 'inclusion_unlimited_args')
self.verify_tag(custom.inclusion_only_unlimited_args, 'inclusion_only_unlimited_args')
self.verify_tag(custom.inclusion_tag_without_context_parameter, 'inclusion_tag_without_context_parameter')
self.verify_tag(custom.inclusion_tag_use_l10n, 'inclusion_tag_use_l10n')
self.verify_tag(custom.inclusion_tag_current_app, 'inclusion_tag_current_app')
self.verify_tag(custom.inclusion_unlimited_args_kwargs, 'inclusion_unlimited_args_kwargs')
def test_15070_current_app(self):
"""
@ -139,6 +280,37 @@ class CustomTagTests(TestCase):
t = template.Template('{% load custom %}{% assignment_params_and_context 37 as var %}The result is: {{ var }}')
self.assertEqual(t.render(c), u'The result is: assignment_params_and_context - Expected result (context value: 42): 37')
t = template.Template('{% load custom %}{% assignment_two_params 37 42 as var %}The result is: {{ var }}')
self.assertEqual(t.render(c), u'The result is: assignment_two_params - Expected result: 37, 42')
t = template.Template('{% load custom %}{% assignment_one_default 37 as var %}The result is: {{ var }}')
self.assertEqual(t.render(c), u'The result is: assignment_one_default - Expected result: 37, hi')
t = template.Template('{% load custom %}{% assignment_one_default 37 two="hello" as var %}The result is: {{ var }}')
self.assertEqual(t.render(c), u'The result is: assignment_one_default - Expected result: 37, hello')
t = template.Template('{% load custom %}{% assignment_one_default one=99 two="hello" as var %}The result is: {{ var }}')
self.assertEqual(t.render(c), u'The result is: assignment_one_default - Expected result: 99, hello')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'assignment_one_default' received unexpected keyword argument 'three'",
template.Template, '{% load custom %}{% assignment_one_default 99 two="hello" three="foo" as var %}')
t = template.Template('{% load custom %}{% assignment_one_default 37 42 as var %}The result is: {{ var }}')
self.assertEqual(t.render(c), u'The result is: assignment_one_default - Expected result: 37, 42')
t = template.Template('{% load custom %}{% assignment_unlimited_args 37 as var %}The result is: {{ var }}')
self.assertEqual(t.render(c), u'The result is: assignment_unlimited_args - Expected result: 37, hi')
t = template.Template('{% load custom %}{% assignment_unlimited_args 37 42 56 89 as var %}The result is: {{ var }}')
self.assertEqual(t.render(c), u'The result is: assignment_unlimited_args - Expected result: 37, 42, 56, 89')
t = template.Template('{% load custom %}{% assignment_only_unlimited_args as var %}The result is: {{ var }}')
self.assertEqual(t.render(c), u'The result is: assignment_only_unlimited_args - Expected result: ')
t = template.Template('{% load custom %}{% assignment_only_unlimited_args 37 42 56 89 as var %}The result is: {{ var }}')
self.assertEqual(t.render(c), u'The result is: assignment_only_unlimited_args - Expected result: 37, 42, 56, 89')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'assignment_one_param' tag takes at least 2 arguments and the second last argument must be 'as'",
template.Template, '{% load custom %}{% assignment_one_param 37 %}The result is: {{ var }}')
@ -151,6 +323,33 @@ class CustomTagTests(TestCase):
"'assignment_one_param' tag takes at least 2 arguments and the second last argument must be 'as'",
template.Template, '{% load custom %}{% assignment_one_param 37 ass var %}The result is: {{ var }}')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'assignment_two_params' received too many positional arguments",
template.Template, '{% load custom %}{% assignment_two_params 37 42 56 as var %}The result is: {{ var }}')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'assignment_one_default' received too many positional arguments",
template.Template, '{% load custom %}{% assignment_one_default 37 42 56 as var %}The result is: {{ var }}')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'assignment_one_default' did not receive value\(s\) for the argument\(s\): 'one'",
template.Template, '{% load custom %}{% assignment_one_default as var %}The result is: {{ var }}')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'assignment_unlimited_args' did not receive value\(s\) for the argument\(s\): 'one'",
template.Template, '{% load custom %}{% assignment_unlimited_args as var %}The result is: {{ var }}')
t = template.Template('{% load custom %}{% assignment_unlimited_args_kwargs 37 40|add:2 56 eggs="scrambled" four=1|add:3 as var %}The result is: {{ var }}')
self.assertEqual(t.render(c), u'The result is: assignment_unlimited_args_kwargs - Expected result: 37, 42, 56 / eggs=scrambled, four=4')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'assignment_unlimited_args_kwargs' received some positional argument\(s\) after some keyword argument\(s\)",
template.Template, '{% load custom %}{% assignment_unlimited_args_kwargs 37 40|add:2 eggs="scrambled" 56 four=1|add:3 as var %}The result is: {{ var }}')
self.assertRaisesRegexp(template.TemplateSyntaxError,
"'assignment_unlimited_args_kwargs' received multiple values for keyword argument 'eggs'",
template.Template, '{% load custom %}{% assignment_unlimited_args_kwargs 37 eggs="scrambled" eggs="scrambled" as var %}The result is: {{ var }}')
def test_assignment_tag_registration(self):
# Test that the decorators preserve the decorated function's docstring, name and attributes.
self.verify_tag(custom.assignment_no_params, 'assignment_no_params')
@ -158,16 +357,16 @@ class CustomTagTests(TestCase):
self.verify_tag(custom.assignment_explicit_no_context, 'assignment_explicit_no_context')
self.verify_tag(custom.assignment_no_params_with_context, 'assignment_no_params_with_context')
self.verify_tag(custom.assignment_params_and_context, 'assignment_params_and_context')
self.verify_tag(custom.assignment_one_default, 'assignment_one_default')
self.verify_tag(custom.assignment_two_params, 'assignment_two_params')
self.verify_tag(custom.assignment_unlimited_args, 'assignment_unlimited_args')
self.verify_tag(custom.assignment_only_unlimited_args, 'assignment_only_unlimited_args')
self.verify_tag(custom.assignment_unlimited_args, 'assignment_unlimited_args')
self.verify_tag(custom.assignment_unlimited_args_kwargs, 'assignment_unlimited_args_kwargs')
self.verify_tag(custom.assignment_tag_without_context_parameter, 'assignment_tag_without_context_parameter')
def test_assignment_tag_missing_context(self):
# That the 'context' parameter must be present when takes_context is True
def an_assignment_tag_without_parameters(arg):
"""Expected __doc__"""
return "Expected result"
register = template.Library()
decorator = register.assignment_tag(takes_context=True)
# The 'context' parameter must be present when takes_context is True
self.assertRaisesRegexp(template.TemplateSyntaxError,
"Any tag function decorated with takes_context=True must have a first argument of 'context'",
decorator, an_assignment_tag_without_parameters)
"'assignment_tag_without_context_parameter' is decorated with takes_context=True so it must have a first argument of 'context'",
template.Template, '{% load custom %}{% assignment_tag_without_context_parameter 123 as var %}')

View File

@ -1,3 +1,5 @@
import operator
from django import template
from django.template.defaultfilters import stringfilter
from django.template.loader import get_template
@ -40,6 +42,61 @@ def params_and_context(context, arg):
return "params_and_context - Expected result (context value: %s): %s" % (context['value'], arg)
params_and_context.anything = "Expected params_and_context __dict__"
@register.simple_tag
def simple_two_params(one, two):
"""Expected simple_two_params __doc__"""
return "simple_two_params - Expected result: %s, %s" % (one, two)
simple_two_params.anything = "Expected simple_two_params __dict__"
@register.simple_tag
def simple_one_default(one, two='hi'):
"""Expected simple_one_default __doc__"""
return "simple_one_default - Expected result: %s, %s" % (one, two)
simple_one_default.anything = "Expected simple_one_default __dict__"
@register.simple_tag
def simple_unlimited_args(one, two='hi', *args):
"""Expected simple_unlimited_args __doc__"""
return "simple_unlimited_args - Expected result: %s" % (', '.join([unicode(arg) for arg in [one, two] + list(args)]))
simple_unlimited_args.anything = "Expected simple_unlimited_args __dict__"
@register.simple_tag
def simple_only_unlimited_args(*args):
"""Expected simple_only_unlimited_args __doc__"""
return "simple_only_unlimited_args - Expected result: %s" % ', '.join([unicode(arg) for arg in args])
simple_only_unlimited_args.anything = "Expected simple_only_unlimited_args __dict__"
@register.simple_tag
def simple_unlimited_args_kwargs(one, two='hi', *args, **kwargs):
"""Expected simple_unlimited_args_kwargs __doc__"""
# Sort the dictionary by key to guarantee the order for testing.
sorted_kwarg = sorted(kwargs.iteritems(), key=operator.itemgetter(0))
return "simple_unlimited_args_kwargs - Expected result: %s / %s" % (
', '.join([unicode(arg) for arg in [one, two] + list(args)]),
', '.join(['%s=%s' % (k, v) for (k, v) in sorted_kwarg])
)
simple_unlimited_args_kwargs.anything = "Expected simple_unlimited_args_kwargs __dict__"
@register.simple_tag(takes_context=True)
def simple_tag_without_context_parameter(arg):
"""Expected simple_tag_without_context_parameter __doc__"""
return "Expected result"
simple_tag_without_context_parameter.anything = "Expected simple_tag_without_context_parameter __dict__"
@register.simple_tag(takes_context=True)
def current_app(context):
return "%s" % context.current_app
@register.simple_tag(takes_context=True)
def use_l10n(context):
return "%s" % context.use_l10n
@register.simple_tag(name='minustwo')
def minustwo_overridden_name(value):
return value - 2
register.simple_tag(lambda x: x - 1, name='minusone')
@register.inclusion_tag('inclusion.html')
def inclusion_no_params():
"""Expected inclusion_no_params __doc__"""
@ -100,21 +157,82 @@ def inclusion_params_and_context_from_template(context, arg):
return {"result" : "inclusion_params_and_context_from_template - Expected result (context value: %s): %s" % (context['value'], arg)}
inclusion_params_and_context_from_template.anything = "Expected inclusion_params_and_context_from_template __dict__"
@register.simple_tag(takes_context=True)
def current_app(context):
return "%s" % context.current_app
@register.inclusion_tag('inclusion.html')
def inclusion_two_params(one, two):
"""Expected inclusion_two_params __doc__"""
return {"result": "inclusion_two_params - Expected result: %s, %s" % (one, two)}
inclusion_two_params.anything = "Expected inclusion_two_params __dict__"
@register.inclusion_tag(get_template('inclusion.html'))
def inclusion_two_params_from_template(one, two):
"""Expected inclusion_two_params_from_template __doc__"""
return {"result": "inclusion_two_params_from_template - Expected result: %s, %s" % (one, two)}
inclusion_two_params_from_template.anything = "Expected inclusion_two_params_from_template __dict__"
@register.inclusion_tag('inclusion.html')
def inclusion_one_default(one, two='hi'):
"""Expected inclusion_one_default __doc__"""
return {"result": "inclusion_one_default - Expected result: %s, %s" % (one, two)}
inclusion_one_default.anything = "Expected inclusion_one_default __dict__"
@register.inclusion_tag(get_template('inclusion.html'))
def inclusion_one_default_from_template(one, two='hi'):
"""Expected inclusion_one_default_from_template __doc__"""
return {"result": "inclusion_one_default_from_template - Expected result: %s, %s" % (one, two)}
inclusion_one_default_from_template.anything = "Expected inclusion_one_default_from_template __dict__"
@register.inclusion_tag('inclusion.html')
def inclusion_unlimited_args(one, two='hi', *args):
"""Expected inclusion_unlimited_args __doc__"""
return {"result": "inclusion_unlimited_args - Expected result: %s" % (', '.join([unicode(arg) for arg in [one, two] + list(args)]))}
inclusion_unlimited_args.anything = "Expected inclusion_unlimited_args __dict__"
@register.inclusion_tag(get_template('inclusion.html'))
def inclusion_unlimited_args_from_template(one, two='hi', *args):
"""Expected inclusion_unlimited_args_from_template __doc__"""
return {"result": "inclusion_unlimited_args_from_template - Expected result: %s" % (', '.join([unicode(arg) for arg in [one, two] + list(args)]))}
inclusion_unlimited_args_from_template.anything = "Expected inclusion_unlimited_args_from_template __dict__"
@register.inclusion_tag('inclusion.html')
def inclusion_only_unlimited_args(*args):
"""Expected inclusion_only_unlimited_args __doc__"""
return {"result": "inclusion_only_unlimited_args - Expected result: %s" % (', '.join([unicode(arg) for arg in args]))}
inclusion_only_unlimited_args.anything = "Expected inclusion_only_unlimited_args __dict__"
@register.inclusion_tag(get_template('inclusion.html'))
def inclusion_only_unlimited_args_from_template(*args):
"""Expected inclusion_only_unlimited_args_from_template __doc__"""
return {"result": "inclusion_only_unlimited_args_from_template - Expected result: %s" % (', '.join([unicode(arg) for arg in args]))}
inclusion_only_unlimited_args_from_template.anything = "Expected inclusion_only_unlimited_args_from_template __dict__"
@register.inclusion_tag('test_incl_tag_current_app.html', takes_context=True)
def inclusion_tag_current_app(context):
"""Expected inclusion_tag_current_app __doc__"""
return {}
@register.simple_tag(takes_context=True)
def use_l10n(context):
return "%s" % context.use_l10n
inclusion_tag_current_app.anything = "Expected inclusion_tag_current_app __dict__"
@register.inclusion_tag('test_incl_tag_use_l10n.html', takes_context=True)
def inclusion_tag_use_l10n(context):
"""Expected inclusion_tag_use_l10n __doc__"""
return {}
inclusion_tag_use_l10n.anything = "Expected inclusion_tag_use_l10n __dict__"
@register.inclusion_tag('inclusion.html')
def inclusion_unlimited_args_kwargs(one, two='hi', *args, **kwargs):
"""Expected inclusion_unlimited_args_kwargs __doc__"""
# Sort the dictionary by key to guarantee the order for testing.
sorted_kwarg = sorted(kwargs.iteritems(), key=operator.itemgetter(0))
return {"result": "inclusion_unlimited_args_kwargs - Expected result: %s / %s" % (
', '.join([unicode(arg) for arg in [one, two] + list(args)]),
', '.join(['%s=%s' % (k, v) for (k, v) in sorted_kwarg])
)}
inclusion_unlimited_args_kwargs.anything = "Expected inclusion_unlimited_args_kwargs __dict__"
@register.inclusion_tag('inclusion.html', takes_context=True)
def inclusion_tag_without_context_parameter(arg):
"""Expected inclusion_tag_without_context_parameter __doc__"""
return {}
inclusion_tag_without_context_parameter.anything = "Expected inclusion_tag_without_context_parameter __dict__"
@register.assignment_tag
def assignment_no_params():
@ -146,8 +264,43 @@ def assignment_params_and_context(context, arg):
return "assignment_params_and_context - Expected result (context value: %s): %s" % (context['value'], arg)
assignment_params_and_context.anything = "Expected assignment_params_and_context __dict__"
register.simple_tag(lambda x: x - 1, name='minusone')
@register.assignment_tag
def assignment_two_params(one, two):
"""Expected assignment_two_params __doc__"""
return "assignment_two_params - Expected result: %s, %s" % (one, two)
assignment_two_params.anything = "Expected assignment_two_params __dict__"
@register.simple_tag(name='minustwo')
def minustwo_overridden_name(value):
return value - 2
@register.assignment_tag
def assignment_one_default(one, two='hi'):
"""Expected assignment_one_default __doc__"""
return "assignment_one_default - Expected result: %s, %s" % (one, two)
assignment_one_default.anything = "Expected assignment_one_default __dict__"
@register.assignment_tag
def assignment_unlimited_args(one, two='hi', *args):
"""Expected assignment_unlimited_args __doc__"""
return "assignment_unlimited_args - Expected result: %s" % (', '.join([unicode(arg) for arg in [one, two] + list(args)]))
assignment_unlimited_args.anything = "Expected assignment_unlimited_args __dict__"
@register.assignment_tag
def assignment_only_unlimited_args(*args):
"""Expected assignment_only_unlimited_args __doc__"""
return "assignment_only_unlimited_args - Expected result: %s" % ', '.join([unicode(arg) for arg in args])
assignment_only_unlimited_args.anything = "Expected assignment_only_unlimited_args __dict__"
@register.assignment_tag
def assignment_unlimited_args_kwargs(one, two='hi', *args, **kwargs):
"""Expected assignment_unlimited_args_kwargs __doc__"""
# Sort the dictionary by key to guarantee the order for testing.
sorted_kwarg = sorted(kwargs.iteritems(), key=operator.itemgetter(0))
return "assignment_unlimited_args_kwargs - Expected result: %s / %s" % (
', '.join([unicode(arg) for arg in [one, two] + list(args)]),
', '.join(['%s=%s' % (k, v) for (k, v) in sorted_kwarg])
)
assignment_unlimited_args_kwargs.anything = "Expected assignment_unlimited_args_kwargs __dict__"
@register.assignment_tag(takes_context=True)
def assignment_tag_without_context_parameter(arg):
"""Expected assignment_tag_without_context_parameter __doc__"""
return "Expected result"
assignment_tag_without_context_parameter.anything = "Expected assignment_tag_without_context_parameter __dict__"