""" Parser and utilities for the smart 'if' tag """ import warnings from django.utils.deprecation import RemovedInDjango110Warning # Using a simple top down parser, as described here: # http://effbot.org/zone/simple-top-down-parsing.htm. # 'led' = left denotation # 'nud' = null denotation # 'bp' = binding power (left = lbp, right = rbp) class TokenBase(object): """ Base class for operators and literals, mainly for debugging and for throwing syntax errors. """ id = None # node/token type name value = None # used by literals first = second = None # used by tree nodes def nud(self, parser): # Null denotation - called in prefix context raise parser.error_class( "Not expecting '%s' in this position in if tag." % self.id ) def led(self, left, parser): # Left denotation - called in infix context raise parser.error_class( "Not expecting '%s' as infix operator in if tag." % self.id ) def display(self): """ Returns what to display in error messages for this node """ return self.id def __repr__(self): out = [str(x) for x in [self.id, self.first, self.second] if x is not None] return "(" + " ".join(out) + ")" def infix(bp, func): """ Creates an infix operator, given a binding power and a function that evaluates the node """ class Operator(TokenBase): lbp = bp def led(self, left, parser): self.first = left self.second = parser.expression(bp) return self def eval(self, context): try: return func(context, self.first, self.second) except Exception: # Templates shouldn't throw exceptions when rendering. We are # most likely to get exceptions for things like {% if foo in bar # %} where 'bar' does not support 'in', so default to False return False return Operator def prefix(bp, func): """ Creates a prefix operator, given a binding power and a function that evaluates the node. """ class Operator(TokenBase): lbp = bp def nud(self, parser): self.first = parser.expression(bp) self.second = None return self def eval(self, context): try: return func(context, self.first) except Exception: return False return Operator # Operator precedence follows Python. # NB - we can get slightly more accurate syntax error messages by not using the # same object for '==' and '='. # We defer variable evaluation to the lambda to ensure that terms are # lazily evaluated using Python's boolean parsing logic. OPERATORS = { 'or': infix(6, lambda context, x, y: x.eval(context) or y.eval(context)), 'and': infix(7, lambda context, x, y: x.eval(context) and y.eval(context)), 'not': prefix(8, lambda context, x: not x.eval(context)), 'in': infix(9, lambda context, x, y: x.eval(context) in y.eval(context)), 'not in': infix(9, lambda context, x, y: x.eval(context) not in y.eval(context)), # This should be removed in Django 1.10: '=': infix(10, lambda context, x, y: x.eval(context) == y.eval(context)), '==': infix(10, lambda context, x, y: x.eval(context) == y.eval(context)), '!=': infix(10, lambda context, x, y: x.eval(context) != y.eval(context)), '>': infix(10, lambda context, x, y: x.eval(context) > y.eval(context)), '>=': infix(10, lambda context, x, y: x.eval(context) >= y.eval(context)), '<': infix(10, lambda context, x, y: x.eval(context) < y.eval(context)), '<=': infix(10, lambda context, x, y: x.eval(context) <= y.eval(context)), } # Assign 'id' to each: for key, op in OPERATORS.items(): op.id = key class Literal(TokenBase): """ A basic self-resolvable object similar to a Django template variable. """ # IfParser uses Literal in create_var, but TemplateIfParser overrides # create_var so that a proper implementation that actually resolves # variables, filters etc is used. id = "literal" lbp = 0 def __init__(self, value): self.value = value def display(self): return repr(self.value) def nud(self, parser): return self def eval(self, context): return self.value def __repr__(self): return "(%s %r)" % (self.id, self.value) class EndToken(TokenBase): lbp = 0 def nud(self, parser): raise parser.error_class("Unexpected end of expression in if tag.") EndToken = EndToken() class IfParser(object): error_class = ValueError def __init__(self, tokens): # pre-pass necessary to turn 'not','in' into single token l = len(tokens) mapped_tokens = [] i = 0 while i < l: token = tokens[i] if token == "not" and i + 1 < l and tokens[i + 1] == "in": token = "not in" i += 1 # skip 'in' mapped_tokens.append(self.translate_token(token)) i += 1 self.tokens = mapped_tokens self.pos = 0 self.current_token = self.next_token() def translate_token(self, token): try: op = OPERATORS[token] except (KeyError, TypeError): return self.create_var(token) else: if token == '=': warnings.warn( "Operator '=' is deprecated and will be removed in Django 1.10. Use '==' instead.", RemovedInDjango110Warning, stacklevel=2 ) return op() def next_token(self): if self.pos >= len(self.tokens): return EndToken else: retval = self.tokens[self.pos] self.pos += 1 return retval def parse(self): retval = self.expression() # Check that we have exhausted all the tokens if self.current_token is not EndToken: raise self.error_class("Unused '%s' at end of if expression." % self.current_token.display()) return retval def expression(self, rbp=0): t = self.current_token self.current_token = self.next_token() left = t.nud(self) while rbp < self.current_token.lbp: t = self.current_token self.current_token = self.next_token() left = t.led(left, self) return left def create_var(self, value): return Literal(value)