smartif.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. """
  2. Parser and utilities for the smart 'if' tag
  3. """
  4. # Using a simple top down parser, as described here:
  5. # http://effbot.org/zone/simple-top-down-parsing.htm.
  6. # 'led' = left denotation
  7. # 'nud' = null denotation
  8. # 'bp' = binding power (left = lbp, right = rbp)
  9. class TokenBase(object):
  10. """
  11. Base class for operators and literals, mainly for debugging and for throwing
  12. syntax errors.
  13. """
  14. id = None # node/token type name
  15. value = None # used by literals
  16. first = second = None # used by tree nodes
  17. def nud(self, parser):
  18. # Null denotation - called in prefix context
  19. raise parser.error_class(
  20. "Not expecting '%s' in this position in if tag." % self.id
  21. )
  22. def led(self, left, parser):
  23. # Left denotation - called in infix context
  24. raise parser.error_class(
  25. "Not expecting '%s' as infix operator in if tag." % self.id
  26. )
  27. def display(self):
  28. """
  29. Returns what to display in error messages for this node
  30. """
  31. return self.id
  32. def __repr__(self):
  33. out = [str(x) for x in [self.id, self.first, self.second] if x is not None]
  34. return "(" + " ".join(out) + ")"
  35. def infix(bp, func):
  36. """
  37. Creates an infix operator, given a binding power and a function that
  38. evaluates the node
  39. """
  40. class Operator(TokenBase):
  41. lbp = bp
  42. def led(self, left, parser):
  43. self.first = left
  44. self.second = parser.expression(bp)
  45. return self
  46. def eval(self, context):
  47. try:
  48. return func(context, self.first, self.second)
  49. except Exception:
  50. # Templates shouldn't throw exceptions when rendering. We are
  51. # most likely to get exceptions for things like {% if foo in bar
  52. # %} where 'bar' does not support 'in', so default to False
  53. return False
  54. return Operator
  55. def prefix(bp, func):
  56. """
  57. Creates a prefix operator, given a binding power and a function that
  58. evaluates the node.
  59. """
  60. class Operator(TokenBase):
  61. lbp = bp
  62. def nud(self, parser):
  63. self.first = parser.expression(bp)
  64. self.second = None
  65. return self
  66. def eval(self, context):
  67. try:
  68. return func(context, self.first)
  69. except Exception:
  70. return False
  71. return Operator
  72. # Operator precedence follows Python.
  73. # NB - we can get slightly more accurate syntax error messages by not using the
  74. # same object for '==' and '='.
  75. # We defer variable evaluation to the lambda to ensure that terms are
  76. # lazily evaluated using Python's boolean parsing logic.
  77. OPERATORS = {
  78. 'or': infix(6, lambda context, x, y: x.eval(context) or y.eval(context)),
  79. 'and': infix(7, lambda context, x, y: x.eval(context) and y.eval(context)),
  80. 'not': prefix(8, lambda context, x: not x.eval(context)),
  81. 'in': infix(9, lambda context, x, y: x.eval(context) in y.eval(context)),
  82. 'not in': infix(9, lambda context, x, y: x.eval(context) not in y.eval(context)),
  83. '=': infix(10, lambda context, x, y: x.eval(context) == y.eval(context)),
  84. '==': infix(10, lambda context, x, y: x.eval(context) == y.eval(context)),
  85. '!=': infix(10, lambda context, x, y: x.eval(context) != y.eval(context)),
  86. '>': infix(10, lambda context, x, y: x.eval(context) > y.eval(context)),
  87. '>=': infix(10, lambda context, x, y: x.eval(context) >= y.eval(context)),
  88. '<': infix(10, lambda context, x, y: x.eval(context) < y.eval(context)),
  89. '<=': infix(10, lambda context, x, y: x.eval(context) <= y.eval(context)),
  90. }
  91. # Assign 'id' to each:
  92. for key, op in OPERATORS.items():
  93. op.id = key
  94. class Literal(TokenBase):
  95. """
  96. A basic self-resolvable object similar to a Django template variable.
  97. """
  98. # IfParser uses Literal in create_var, but TemplateIfParser overrides
  99. # create_var so that a proper implementation that actually resolves
  100. # variables, filters etc is used.
  101. id = "literal"
  102. lbp = 0
  103. def __init__(self, value):
  104. self.value = value
  105. def display(self):
  106. return repr(self.value)
  107. def nud(self, parser):
  108. return self
  109. def eval(self, context):
  110. return self.value
  111. def __repr__(self):
  112. return "(%s %r)" % (self.id, self.value)
  113. class EndToken(TokenBase):
  114. lbp = 0
  115. def nud(self, parser):
  116. raise parser.error_class("Unexpected end of expression in if tag.")
  117. EndToken = EndToken()
  118. class IfParser(object):
  119. error_class = ValueError
  120. def __init__(self, tokens):
  121. # pre-pass necessary to turn 'not','in' into single token
  122. l = len(tokens)
  123. mapped_tokens = []
  124. i = 0
  125. while i < l:
  126. token = tokens[i]
  127. if token == "not" and i + 1 < l and tokens[i + 1] == "in":
  128. token = "not in"
  129. i += 1 # skip 'in'
  130. mapped_tokens.append(self.translate_token(token))
  131. i += 1
  132. self.tokens = mapped_tokens
  133. self.pos = 0
  134. self.current_token = self.next_token()
  135. def translate_token(self, token):
  136. try:
  137. op = OPERATORS[token]
  138. except (KeyError, TypeError):
  139. return self.create_var(token)
  140. else:
  141. return op()
  142. def next_token(self):
  143. if self.pos >= len(self.tokens):
  144. return EndToken
  145. else:
  146. retval = self.tokens[self.pos]
  147. self.pos += 1
  148. return retval
  149. def parse(self):
  150. retval = self.expression()
  151. # Check that we have exhausted all the tokens
  152. if self.current_token is not EndToken:
  153. raise self.error_class("Unused '%s' at end of if expression." %
  154. self.current_token.display())
  155. return retval
  156. def expression(self, rbp=0):
  157. t = self.current_token
  158. self.current_token = self.next_token()
  159. left = t.nud(self)
  160. while rbp < self.current_token.lbp:
  161. t = self.current_token
  162. self.current_token = self.next_token()
  163. left = t.led(left, self)
  164. return left
  165. def create_var(self, value):
  166. return Literal(value)