Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Fixed #13956 -- Enabled `*args` and `**kwargs` support for `simple_ta…

…g`, `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
  • Loading branch information...
commit 8137027fd770ceb02d593605b22b8cfff1ef2e66 1 parent 29b8e34
Julien Phalip jphalip authored
560 django/template/base.py
View
@@ -3,13 +3,16 @@
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_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 @@
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 @@ def __str__(self):
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 @@ def reload(self):
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 @@ def __init__(self, template_string, origin):
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 @@ def create_token(self, token_string, in_tag):
"""
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 @@ def __init__(self, tokens):
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 @@ def parse(self, parse_until=None):
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 @@ def extend_nodelist(self, nodelist, node, token):
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 @@ def empty_block_tag(self, token):
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 @@ def add_library(self, lib):
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 @@ def find_filter(self, filter_name):
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 @@ def __init__(self, subject):
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 @@ def tag(self):
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 @@ def next_space_index(subject, i):
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 @@ def next_space_index(subject, i):
%(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 @@ def next_space_index(subject, i):
)""" % {
'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 @@ def __init__(self, token, parser):
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 @@ def __init__(self, token, parser):
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 @@ def __init__(self, token, parser):
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 @@ def args_check(name, func, provided):
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 @@ def args_check(name, func, provided):
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 @@ def __init__(self, var):
# 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 @@ def _resolve_lookup(self, context):
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 @@ def _resolve_lookup(self, context):
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 @@ def render(self, context):
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:
+ 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:
- message = "%s takes between %s and %s arguments" % (name, bmin, bmax)
- raise TemplateSyntaxError(message)
- return node_class(bits)
+ 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 @@ def __init__(self):
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 @@ def tag(self, name=None, compile_function=None):
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 @@ def filter(self, name=None, filter_func=None):
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 @@ def filter_function(self, func):
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)
-
- function_name = name or getattr(func, '_decorated_function', func).__name__
- compile_func = partial(generic_tag_compiler, params, defaults, function_name, SimpleNode)
+ 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=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 @@ def render(self, context):
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 @@ def compile_func(parser, token):
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 @@ def render(self, context):
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')
53 django/template/defaulttags.py
View
@@ -10,64 +10,13 @@
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."""
67 docs/howto/custom-template-tags.txt
View
@@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
25 docs/releases/1.4.txt
View
@@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
235 tests/regressiontests/templates/custom.py
View
@@ -35,6 +35,56 @@ def test_simple_tags(self):
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 @@ def test_simple_tag_registration(self):
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 @@ def test_inclusion_tags(self):
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 @@ def test_inclusion_tags_from_template(self):
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 @@ def test_inclusion_tag_registration(self):
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 @@ def test_assignment_tags(self):
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 @@ def test_assignment_tags(self):
"'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 @@ def test_assignment_tag_registration(self):
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 %}')
175 tests/regressiontests/templates/templatetags/custom.py
View
@@ -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__"
Please sign in to comment.
Something went wrong with that request. Please try again.