From 8137027fd770ceb02d593605b22b8cfff1ef2e66 Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Tue, 27 Sep 2011 12:15:15 +0000 Subject: [PATCH] =?UTF-8?q?Fixed=20#13956=20--=20Enabled=20`*args`=20and?= =?UTF-8?q?=20`**kwargs`=20support=20for=20`simple=5Ftag`,=20`inclusion=5F?= =?UTF-8?q?tag`=20and=20`assignment=5Ftag`.=20Many=20thanks=20to=20Stephen?= =?UTF-8?q?=20Burrows=20for=20the=20report=20and=20initial=20patch,=20to?= =?UTF-8?q?=20Gregor=20Mu=CC=88llegger=20for=20the=20initial=20tests,=20to?= =?UTF-8?q?=20SamBull=20for=20the=20suggestions,=20and=20to=20Jannis=20Lei?= =?UTF-8?q?del=20for=20the=20review=20and=20PEP8=20cleanup.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://code.djangoproject.com/svn/django/trunk@16908 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/template/base.py | 558 ++++++++++++------ django/template/defaulttags.py | 53 +- docs/howto/custom-template-tags.txt | 67 +++ docs/releases/1.4.txt | 25 + tests/regressiontests/templates/custom.py | 235 +++++++- .../templates/templatetags/custom.py | 175 +++++- 6 files changed, 848 insertions(+), 265 deletions(-) diff --git a/django/template/base.py b/django/template/base.py index c94eeb51f9..8642f4d4f5 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -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 = '' -# 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=''): + def __init__(self, template_string, origin=None, + name=''): 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') diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 0b039a5e00..9620d1cb81 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -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.""" diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index eaab36c293..4001794680 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -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`. +``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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index 9cf268a869..29fd5ef49f 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -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`, :ref:`inclusion_tag +` and the newly introduced +:ref:`assignment_tag` 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/templates/custom.py b/tests/regressiontests/templates/custom.py index d781874436..c05229e6f2 100644 --- a/tests/regressiontests/templates/custom.py +++ b/tests/regressiontests/templates/custom.py @@ -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 %}') diff --git a/tests/regressiontests/templates/templatetags/custom.py b/tests/regressiontests/templates/templatetags/custom.py index dfa4171eb0..86206617b8 100644 --- a/tests/regressiontests/templates/templatetags/custom.py +++ b/tests/regressiontests/templates/templatetags/custom.py @@ -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__" \ No newline at end of file