| from __future__ import absolute_import |
| |
| import re |
| from functools import partial |
| from inspect import getargspec |
| |
| from django.conf import settings |
| 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.encoding import smart_unicode, force_unicode, smart_str |
| from django.utils.translation import ugettext_lazy, pgettext_lazy |
| 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 |
| from django.utils.timezone import localtime |
| |
| |
| 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 = '|' |
| FILTER_ARGUMENT_SEPARATOR = ':' |
| VARIABLE_ATTRIBUTE_SEPARATOR = '.' |
| BLOCK_TAG_START = '{%' |
| BLOCK_TAG_END = '%}' |
| VARIABLE_TAG_START = '{{' |
| VARIABLE_TAG_END = '}}' |
| COMMENT_TAG_START = '{#' |
| COMMENT_TAG_END = '#}' |
| TRANSLATOR_COMMENT_MARK = 'Translators' |
| SINGLE_BRACE_START = '{' |
| SINGLE_BRACE_END = '}' |
| |
| 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)))) |
| |
| # global dictionary of libraries that have been loaded using get_library |
| libraries = {} |
| # global list of libraries to load by default for a new parser |
| builtins = [] |
| |
| # True if TEMPLATE_STRING_IF_INVALID contains a format string (%s). None means |
| # uninitialised. |
| invalid_var_format_string = None |
| |
| class TemplateSyntaxError(Exception): |
| pass |
| |
| class TemplateDoesNotExist(Exception): |
| pass |
| |
| class TemplateEncodingError(Exception): |
| pass |
| |
| class VariableDoesNotExist(Exception): |
| |
| def __init__(self, msg, params=()): |
| self.msg = msg |
| self.params = params |
| |
| 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]) |
| |
| class InvalidTemplateLibrary(Exception): |
| pass |
| |
| class Origin(object): |
| def __init__(self, name): |
| self.name = name |
| |
| def reload(self): |
| raise NotImplementedError |
| |
| def __str__(self): |
| return self.name |
| |
| class StringOrigin(Origin): |
| def __init__(self, source): |
| super(StringOrigin, self).__init__(UNKNOWN_SOURCE) |
| self.source = source |
| |
| def reload(self): |
| return self.source |
| |
| class Template(object): |
| 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.") |
| if settings.TEMPLATE_DEBUG and origin is None: |
| origin = StringOrigin(template_string) |
| self.nodelist = compile_string(template_string, origin) |
| self.name = name |
| |
| def __iter__(self): |
| for node in self.nodelist: |
| for subnode in node: |
| yield subnode |
| |
| def _render(self, context): |
| return self.nodelist.render(context) |
| |
| def render(self, context): |
| "Display stage -- can be called many times" |
| context.render_context.push() |
| try: |
| return self._render(context) |
| finally: |
| context.render_context.pop() |
| |
| def compile_string(template_string, origin): |
| "Compiles template_string into NodeList ready for rendering" |
| if settings.TEMPLATE_DEBUG: |
| from django.template.debug import DebugLexer, DebugParser |
| lexer_class, parser_class = DebugLexer, DebugParser |
| else: |
| lexer_class, parser_class = Lexer, Parser |
| lexer = lexer_class(template_string, origin) |
| parser = parser_class(lexer.tokenize()) |
| return parser.parse() |
| |
| class Token(object): |
| def __init__(self, token_type, contents): |
| # 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): |
| token_name = TOKEN_MAPPING[self.token_type] |
| return ('<%s token: "%s...">' % |
| (token_name, self.contents[:20].replace('\n', ''))) |
| |
| def split_contents(self): |
| split = [] |
| bits = iter(smart_split(self.contents)) |
| for bit in bits: |
| # Handle translation-marked template pieces |
| if bit.startswith('_("') or bit.startswith("_('"): |
| sentinal = bit[2] + ')' |
| trans_bit = [bit] |
| while not bit.endswith(sentinal): |
| bit = bits.next() |
| trans_bit.append(bit) |
| bit = ' '.join(trans_bit) |
| split.append(bit) |
| return split |
| |
| class Lexer(object): |
| def __init__(self, template_string, origin): |
| self.template_string = template_string |
| self.origin = origin |
| self.lineno = 1 |
| |
| def tokenize(self): |
| """ |
| Return a list of tokens from a given template_string. |
| """ |
| in_tag = False |
| result = [] |
| for bit in tag_re.split(self.template_string): |
| if bit: |
| result.append(self.create_token(bit, in_tag)) |
| in_tag = not in_tag |
| return result |
| |
| def create_token(self, token_string, in_tag): |
| """ |
| Convert the given token string into a new Token object and return it. |
| If in_tag is True, we are processing something that matched a tag, |
| otherwise it should be treated as a literal string. |
| """ |
| if in_tag: |
| # The [2:-2] ranges below strip off *_TAG_START and *_TAG_END. |
| # We could do len(BLOCK_TAG_START) to be more "correct", but we've |
| # hard-coded the 2s here for performance. And it's not like |
| # the TAG_START values are going to change anytime, anyway. |
| if token_string.startswith(VARIABLE_TAG_START): |
| token = Token(TOKEN_VAR, token_string[2:-2].strip()) |
| elif token_string.startswith(BLOCK_TAG_START): |
| token = Token(TOKEN_BLOCK, token_string[2:-2].strip()) |
| elif token_string.startswith(COMMENT_TAG_START): |
| content = '' |
| if token_string.find(TRANSLATOR_COMMENT_MARK): |
| content = token_string[2:-2].strip() |
| token = Token(TOKEN_COMMENT, content) |
| else: |
| token = Token(TOKEN_TEXT, token_string) |
| token.lineno = self.lineno |
| self.lineno += token_string.count('\n') |
| return token |
| |
| class Parser(object): |
| def __init__(self, tokens): |
| self.tokens = tokens |
| self.tags = {} |
| self.filters = {} |
| for lib in builtins: |
| self.add_library(lib) |
| |
| def parse(self, parse_until=None): |
| if parse_until is None: |
| parse_until = [] |
| nodelist = self.create_nodelist() |
| while self.tokens: |
| token = self.next_token() |
| # Use the raw values here for TOKEN_* for a tiny performance boost. |
| if token.token_type == 0: # TOKEN_TEXT |
| self.extend_nodelist(nodelist, TextNode(token.contents), token) |
| elif token.token_type == 1: # TOKEN_VAR |
| if not token.contents: |
| 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) |
| elif token.token_type == 2: # TOKEN_BLOCK |
| try: |
| command = token.contents.split()[0] |
| except IndexError: |
| self.empty_block_tag(token) |
| if command in parse_until: |
| # put token back on token list so calling |
| # code knows why it terminated |
| self.prepend_token(token) |
| return nodelist |
| # execute callback function for this tag and append |
| # resulting node |
| self.enter_command(command, token) |
| try: |
| compile_func = self.tags[command] |
| except KeyError: |
| self.invalid_block_tag(token, command, parse_until) |
| try: |
| compiled_result = compile_func(self, token) |
| except TemplateSyntaxError, e: |
| if not self.compile_function_error(token, e): |
| raise |
| self.extend_nodelist(nodelist, compiled_result, token) |
| self.exit_command() |
| if parse_until: |
| self.unclosed_block_tag(parse_until) |
| return nodelist |
| |
| def skip_past(self, endtag): |
| while self.tokens: |
| token = self.next_token() |
| if token.token_type == TOKEN_BLOCK and token.contents == endtag: |
| return |
| self.unclosed_block_tag([endtag]) |
| |
| def create_variable_node(self, filter_expression): |
| return VariableNode(filter_expression) |
| |
| def create_nodelist(self): |
| return NodeList() |
| |
| def extend_nodelist(self, nodelist, node, token): |
| if node.must_be_first and nodelist: |
| try: |
| if nodelist.contains_nontext: |
| raise AttributeError |
| except AttributeError: |
| 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) |
| |
| def enter_command(self, command, token): |
| pass |
| |
| def exit_command(self): |
| pass |
| |
| def error(self, token, msg): |
| return TemplateSyntaxError(msg) |
| |
| def empty_variable(self, token): |
| raise self.error(token, "Empty variable tag") |
| |
| def empty_block_tag(self, token): |
| raise self.error(token, "Empty block tag") |
| |
| 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'" % command) |
| |
| def unclosed_block_tag(self, parse_until): |
| raise self.error(None, "Unclosed tags: %s " % ', '.join(parse_until)) |
| |
| def compile_function_error(self, token, e): |
| pass |
| |
| def next_token(self): |
| return self.tokens.pop(0) |
| |
| def prepend_token(self, token): |
| self.tokens.insert(0, token) |
| |
| def delete_first_token(self): |
| del self.tokens[0] |
| |
| def add_library(self, lib): |
| self.tags.update(lib.tags) |
| self.filters.update(lib.filters) |
| |
| def compile_filter(self, token): |
| """ |
| Convenient wrapper for FilterExpression |
| """ |
| return FilterExpression(token, self) |
| |
| def find_filter(self, filter_name): |
| if filter_name in self.filters: |
| return self.filters[filter_name] |
| else: |
| raise TemplateSyntaxError("Invalid filter: '%s'" % 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. |
| |
| The parser's "tagname" instance-variable stores the name of the tag that |
| the filter was called with. |
| """ |
| def __init__(self, subject): |
| self.subject = subject |
| self.pointer = 0 |
| self.backout = [] |
| self.tagname = self.tag() |
| |
| def top(self): |
| """ |
| 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. |
| """ |
| return self.pointer < len(self.subject) |
| |
| def back(self): |
| """ |
| Undoes the last microparser. Use this for lookahead and backtracking. |
| """ |
| if not len(self.backout): |
| 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. |
| """ |
| subject = self.subject |
| i = self.pointer |
| if i >= len(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 |
| s = subject[p:i] |
| while i < len(subject) and subject[i] in (' ', '\t'): |
| i += 1 |
| self.backout.append(self.pointer) |
| self.pointer = i |
| return s |
| |
| def value(self): |
| """ |
| 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 |
| """ |
| while i < len(subject) and subject[i] not in (' ', '\t'): |
| if subject[i] in ('"', "'"): |
| c = subject[i] |
| i += 1 |
| 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)) |
| i += 1 |
| return i |
| |
| if i >= len(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)) |
| i += 1 |
| |
| # Continue parsing until next "real" space, |
| # so that filters are also included |
| i = next_space_index(subject, i) |
| |
| res = subject[p:i] |
| while i < len(subject) and subject[i] in (' ', '\t'): |
| i += 1 |
| self.backout.append(self.pointer) |
| self.pointer = i |
| return res |
| else: |
| p = i |
| i = next_space_index(subject, i) |
| s = subject[p:i] |
| while i < len(subject) and subject[i] in (' ', '\t'): |
| i += 1 |
| self.backout.append(self.pointer) |
| self.pointer = i |
| return s |
| |
| # This only matches constant *strings* (things in quotes or marked for |
| # translation). Numbers are treated as variables for implementation reasons |
| # (so that they retain their type when passed to filters). |
| constant_string = r""" |
| (?:%(i18n_open)s%(strdq)s%(i18n_close)s| |
| %(i18n_open)s%(strsq)s%(i18n_close)s| |
| %(strdq)s| |
| %(strsq)s) |
| """ % { |
| 'strdq': r'"[^"\\]*(?:\\.[^"\\]*)*"', # double-quoted string |
| 'strsq': r"'[^'\\]*(?:\\.[^'\\]*)*'", # single-quoted string |
| 'i18n_open': re.escape("_("), |
| 'i18n_close': re.escape(")"), |
| } |
| constant_string = constant_string.replace("\n", "") |
| |
| filter_raw_string = r""" |
| ^(?P<constant>%(constant)s)| |
| ^(?P<var>[%(var_chars)s]+|%(num)s)| |
| (?:%(filter_sep)s |
| (?P<filter_name>\w+) |
| (?:%(arg_sep)s |
| (?: |
| (?P<constant_arg>%(constant)s)| |
| (?P<var_arg>[%(var_chars)s]+|%(num)s) |
| ) |
| )? |
| )""" % { |
| 'constant': constant_string, |
| 'num': r'[-+\.]?\d[\d\.e]*', |
| '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) |
| |
| class FilterExpression(object): |
| """ |
| 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:: |
| |
| >>> token = 'variable|default:"Default value"|date:"Y-m-d"' |
| >>> p = Parser('') |
| >>> fe = FilterExpression(token, p) |
| >>> len(fe.filters) |
| 2 |
| >>> fe.var |
| <Variable: 'variable'> |
| |
| This class should never be instantiated outside of the |
| get_filters_from_token helper function. |
| """ |
| def __init__(self, token, parser): |
| self.token = token |
| matches = filter_re.finditer(token) |
| var_obj = None |
| filters = [] |
| upto = 0 |
| 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:])) |
| if var_obj is None: |
| var, constant = match.group("var", "constant") |
| if constant: |
| try: |
| var_obj = Variable(constant).resolve({}) |
| except VariableDoesNotExist: |
| var_obj = None |
| elif var is None: |
| raise TemplateSyntaxError("Could not find variable at " |
| "start of %s." % token) |
| else: |
| var_obj = Variable(var) |
| else: |
| filter_name = match.group("filter_name") |
| args = [] |
| constant_arg, var_arg = match.group("constant_arg", "var_arg") |
| if constant_arg: |
| args.append((False, Variable(constant_arg).resolve({}))) |
| elif var_arg: |
| args.append((True, Variable(var_arg))) |
| filter_func = parser.find_filter(filter_name) |
| self.args_check(filter_name, filter_func, args) |
| 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)) |
| |
| self.filters = filters |
| self.var = var_obj |
| |
| def resolve(self, context, ignore_failures=False): |
| if isinstance(self.var, Variable): |
| try: |
| obj = self.var.resolve(context) |
| except VariableDoesNotExist: |
| if ignore_failures: |
| obj = None |
| else: |
| if settings.TEMPLATE_STRING_IF_INVALID: |
| global invalid_var_format_string |
| if invalid_var_format_string is None: |
| invalid_var_format_string = '%s' in settings.TEMPLATE_STRING_IF_INVALID |
| if invalid_var_format_string: |
| return settings.TEMPLATE_STRING_IF_INVALID % self.var |
| return settings.TEMPLATE_STRING_IF_INVALID |
| else: |
| obj = settings.TEMPLATE_STRING_IF_INVALID |
| else: |
| obj = self.var |
| for func, args in self.filters: |
| arg_vals = [] |
| for lookup, arg in args: |
| if not lookup: |
| arg_vals.append(mark_safe(arg)) |
| else: |
| arg_vals.append(arg.resolve(context)) |
| if getattr(func, 'expects_localtime', False): |
| obj = localtime(obj, context.use_tz) |
| if getattr(func, 'needs_autoescape', False): |
| new_obj = func(obj, autoescape=context.autoescape, *arg_vals) |
| else: |
| new_obj = func(obj, *arg_vals) |
| if getattr(func, 'is_safe', False) and isinstance(obj, SafeData): |
| obj = mark_safe(new_obj) |
| elif isinstance(obj, EscapeData): |
| obj = mark_for_escaping(new_obj) |
| else: |
| obj = new_obj |
| return obj |
| |
| def args_check(name, func, provided): |
| provided = list(provided) |
| plen = len(provided) |
| # Check to see if a decorator is providing the real function. |
| func = getattr(func, '_decorated_function', func) |
| args, varargs, varkw, defaults = getargspec(func) |
| # First argument is filter input. |
| args.pop(0) |
| if defaults: |
| nondefs = args[:-len(defaults)] |
| else: |
| nondefs = args |
| # Args without defaults must be provided. |
| try: |
| for arg in nondefs: |
| provided.pop(0) |
| except IndexError: |
| # Not enough |
| raise TemplateSyntaxError("%s requires %d arguments, %d provided" % |
| (name, len(nondefs), plen)) |
| |
| # Defaults can be overridden. |
| defaults = defaults and list(defaults) or [] |
| try: |
| for parg in provided: |
| defaults.pop(0) |
| except IndexError: |
| # Too many. |
| raise TemplateSyntaxError("%s requires %d arguments, %d provided" % |
| (name, len(nondefs), plen)) |
| |
| return True |
| args_check = staticmethod(args_check) |
| |
| def __str__(self): |
| return self.token |
| |
| def resolve_variable(path, context): |
| """ |
| Returns the resolved variable, which may contain attribute syntax, within |
| the given context. |
| |
| Deprecated; use the Variable class instead. |
| """ |
| return Variable(path).resolve(context) |
| |
| class Variable(object): |
| """ |
| 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'}} |
| >>> Variable('article.section').resolve(c) |
| u'News' |
| >>> Variable('article').resolve(c) |
| {'section': u'News'} |
| >>> class AClass: pass |
| >>> c = AClass() |
| >>> c.article = AClass() |
| >>> c.article.section = u'News' |
| |
| (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.') |
| """ |
| |
| def __init__(self, var): |
| self.var = var |
| self.literal = None |
| self.lookups = None |
| self.translate = False |
| self.message_context = None |
| |
| try: |
| # First try to treat this variable as a number. |
| # |
| # Note that this could cause an OverflowError here that we're not |
| # catching. Since this should only happen at compile time, that's |
| # probably OK. |
| self.literal = float(var) |
| |
| # So it's a float... is it an int? If the original value contained a |
| # dot or an "e" then it was a float, not an int. |
| if '.' not in var and 'e' not in var.lower(): |
| self.literal = int(self.literal) |
| |
| # "2." is invalid |
| if var.endswith('.'): |
| raise ValueError |
| |
| except ValueError: |
| # A ValueError means that the variable isn't a number. |
| if var.startswith('_(') and var.endswith(')'): |
| # The result of the lookup should be translated at rendering |
| # time. |
| self.translate = True |
| var = var[2:-1] |
| # If it's wrapped with quotes (single or double), then |
| # we're also dealing with a literal. |
| try: |
| self.literal = mark_safe(unescape_string_literal(var)) |
| except ValueError: |
| # 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) |
| self.lookups = tuple(var.split(VARIABLE_ATTRIBUTE_SEPARATOR)) |
| |
| def resolve(self, context): |
| """Resolve this variable against a given context.""" |
| if self.lookups is not None: |
| # We're dealing with a variable that needs to be resolved |
| value = self._resolve_lookup(context) |
| else: |
| # We're dealing with a literal, so it's already been "resolved" |
| value = self.literal |
| if self.translate: |
| if self.message_context: |
| return pgettext_lazy(self.message_context, value) |
| else: |
| return ugettext_lazy(value) |
| return value |
| |
| def __repr__(self): |
| return "<%s: %r>" % (self.__class__.__name__, self.var) |
| |
| def __str__(self): |
| return self.var |
| |
| def _resolve_lookup(self, context): |
| """ |
| Performs resolution of a real variable (i.e. not a literal) against the |
| given context. |
| |
| As indicated by the method's name, this method is an implementation |
| detail and shouldn't be called by external code. Use Variable.resolve() |
| instead. |
| """ |
| current = context |
| try: # catch-all for silent variable failures |
| for bit in self.lookups: |
| try: # dictionary lookup |
| current = current[bit] |
| except (TypeError, AttributeError, KeyError): |
| try: # attribute lookup |
| current = getattr(current, bit) |
| except (TypeError, AttributeError): |
| 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 |
| if callable(current): |
| if getattr(current, 'do_not_call_in_templates', False): |
| pass |
| elif getattr(current, 'alters_data', False): |
| current = settings.TEMPLATE_STRING_IF_INVALID |
| else: |
| try: # method call (assuming no args required) |
| current = current() |
| 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 |
| except Exception, e: |
| if getattr(e, 'silent_variable_failure', False): |
| current = settings.TEMPLATE_STRING_IF_INVALID |
| else: |
| raise |
| |
| return current |
| |
| class Node(object): |
| # Set this to True for nodes that must be first in the template (although |
| # they can be preceded by text nodes. |
| must_be_first = False |
| child_nodelists = ('nodelist',) |
| |
| def render(self, context): |
| """ |
| 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 |
| """ |
| nodes = [] |
| if isinstance(self, nodetype): |
| nodes.append(self) |
| for attr in self.child_nodelists: |
| nodelist = getattr(self, attr, None) |
| if nodelist: |
| nodes.extend(nodelist.get_nodes_by_type(nodetype)) |
| return nodes |
| |
| class NodeList(list): |
| # Set to True the first time a non-TextNode is inserted by |
| # extend_nodelist(). |
| contains_nontext = False |
| |
| def render(self, context): |
| bits = [] |
| for node in self: |
| if isinstance(node, Node): |
| bit = self.render_node(node, context) |
| else: |
| bit = node |
| bits.append(force_unicode(bit)) |
| return mark_safe(u''.join(bits)) |
| |
| def get_nodes_by_type(self, nodetype): |
| "Return a list of all nodes of the given type" |
| nodes = [] |
| for node in self: |
| nodes.extend(node.get_nodes_by_type(nodetype)) |
| return nodes |
| |
| def render_node(self, node, context): |
| return node.render(context) |
| |
| class TextNode(Node): |
| def __init__(self, s): |
| self.s = s |
| |
| def __repr__(self): |
| return "<Text Node: '%s'>" % smart_str(self.s[:25], 'ascii', |
| errors='replace') |
| |
| def render(self, context): |
| return self.s |
| |
| def _render_value_in_context(value, context): |
| """ |
| Converts any value to a string to become part of a rendered template. This |
| means escaping, if required, and conversion to a unicode object. If value |
| is a string, it is expected to have already been translated. |
| """ |
| value = localtime(value, use_tz=context.use_tz) |
| value = localize(value, use_l10n=context.use_l10n) |
| value = force_unicode(value) |
| if ((context.autoescape and not isinstance(value, SafeData)) or |
| isinstance(value, EscapeData)): |
| return escape(value) |
| else: |
| return value |
| |
| class VariableNode(Node): |
| def __init__(self, filter_expression): |
| self.filter_expression = filter_expression |
| |
| def __repr__(self): |
| return "<Variable Node: %s>" % self.filter_expression |
| |
| def render(self, context): |
| try: |
| 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. |
| return '' |
| return _render_value_in_context(output, context) |
| |
| # 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: |
| 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): |
| self.filters = {} |
| self.tags = {} |
| |
| def tag(self, name=None, compile_function=None): |
| if name is None and compile_function is None: |
| # @register.tag() |
| return self.tag_function |
| elif name is not None and compile_function is None: |
| if callable(name): |
| # @register.tag |
| return self.tag_function(name) |
| else: |
| # @register.tag('somename') or @register.tag(name='somename') |
| def dec(func): |
| return self.tag(name, func) |
| return dec |
| elif name is not None and compile_function is not None: |
| # register.tag('somename', somefunc) |
| self.tags[name] = compile_function |
| return compile_function |
| else: |
| raise InvalidTemplateLibrary("Unsupported arguments to " |
| "Library.tag: (%r, %r)", (name, compile_function)) |
| |
| def tag_function(self, func): |
| self.tags[getattr(func, "_decorated_function", func).__name__] = func |
| return func |
| |
| def filter(self, name=None, filter_func=None, **flags): |
| if name is None and filter_func is None: |
| # @register.filter() |
| def dec(func): |
| return self.filter_function(func, **flags) |
| return dec |
| |
| elif name is not None and filter_func is None: |
| if callable(name): |
| # @register.filter |
| return self.filter_function(name, **flags) |
| else: |
| # @register.filter('somename') or @register.filter(name='somename') |
| def dec(func): |
| return self.filter(name, func, **flags) |
| return dec |
| |
| elif name is not None and filter_func is not None: |
| # register.filter('somename', somefunc) |
| self.filters[name] = filter_func |
| for attr in ('expects_localtime', 'is_safe', 'needs_autoescape'): |
| if attr in flags: |
| value = flags[attr] |
| # set the flag on the filter for FilterExpression.resolve |
| setattr(filter_func, attr, value) |
| # set the flag on the innermost decorated function |
| # for decorators that need it e.g. stringfilter |
| if hasattr(filter_func, "_decorated_function"): |
| setattr(filter_func._decorated_function, attr, value) |
| return filter_func |
| else: |
| raise InvalidTemplateLibrary("Unsupported arguments to " |
| "Library.filter: (%r, %r)", (name, filter_func)) |
| |
| def filter_function(self, func, **flags): |
| name = getattr(func, "_decorated_function", func).__name__ |
| return self.filter(name, func, **flags) |
| |
| def simple_tag(self, func=None, takes_context=None, name=None): |
| def dec(func): |
| params, varargs, varkw, defaults = getargspec(func) |
| |
| class SimpleNode(TagHelperNode): |
| |
| def render(self, context): |
| 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 |
| |
| if func is None: |
| # @register.simple_tag(...) |
| return dec |
| elif callable(func): |
| # @register.simple_tag |
| return dec(func) |
| else: |
| raise TemplateSyntaxError("Invalid arguments provided to simple_tag") |
| |
| def assignment_tag(self, func=None, takes_context=None, name=None): |
| def dec(func): |
| params, varargs, varkw, defaults = getargspec(func) |
| |
| 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_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()[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'" % function_name) |
| target_var = bits[-1] |
| 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) |
| |
| compile_func.__doc__ = func.__doc__ |
| self.tag(function_name, compile_func) |
| return func |
| |
| if func is None: |
| # @register.assignment_tag(...) |
| return dec |
| elif callable(func): |
| # @register.assignment_tag |
| return dec(func) |
| else: |
| raise TemplateSyntaxError("Invalid arguments provided to assignment_tag") |
| |
| def inclusion_tag(self, file_name, context_class=Context, takes_context=False, name=None): |
| def dec(func): |
| params, varargs, varkw, defaults = getargspec(func) |
| |
| class InclusionNode(TagHelperNode): |
| |
| def render(self, context): |
| 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 |
| if isinstance(file_name, Template): |
| t = file_name |
| elif not isinstance(file_name, basestring) and is_iterable(file_name): |
| t = select_template(file_name) |
| else: |
| t = get_template(file_name) |
| self.nodelist = t.nodelist |
| new_context = context_class(_dict, **{ |
| 'autoescape': context.autoescape, |
| 'current_app': context.current_app, |
| 'use_l10n': context.use_l10n, |
| 'use_tz': context.use_tz, |
| }) |
| # 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=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 is_library_missing(name): |
| """Check if library that failed to load cannot be found under any |
| templatetags directory or does exist but fails to import. |
| |
| Non-existing condition is checked recursively for each subpackage in cases |
| like <appdir>/templatetags/subpackage/package/module.py. |
| """ |
| # Don't bother to check if '.' is in name since any name will be prefixed |
| # with some template root. |
| path, module = name.rsplit('.', 1) |
| try: |
| package = import_module(path) |
| return not module_has_submodule(package, module) |
| except ImportError: |
| return is_library_missing(path) |
| |
| def import_library(taglib_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 |
| """ |
| 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 is_library_missing(taglib_module): |
| return None |
| else: |
| 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) |
| |
| templatetags_modules = [] |
| |
| def get_templatetags_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. |
| for app_module in ['django'] + list(settings.INSTALLED_APPS): |
| try: |
| templatetag_module = '%s.templatetags' % app_module |
| import_module(templatetag_module) |
| _templatetags_modules.append(templatetag_module) |
| except ImportError: |
| continue |
| templatetags_modules = _templatetags_modules |
| return templatetags_modules |
| |
| 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. |
| |
| {% load somelib %} and {% load someotherlib %} loops twice. |
| |
| 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: |
| templatetags_modules = get_templatetags_modules() |
| tried_modules = [] |
| for module in templatetags_modules: |
| taglib_module = '%s.%s' % (module, library_name) |
| tried_modules.append(taglib_module) |
| lib = import_library(taglib_module) |
| if lib: |
| libraries[library_name] = lib |
| break |
| if not lib: |
| 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') |