You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

smartif.py 6.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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:
  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. Return 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. Create 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. Create 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. # We defer variable evaluation to the lambda to ensure that terms are
  74. # lazily evaluated using Python's boolean parsing logic.
  75. OPERATORS = {
  76. 'or': infix(6, lambda context, x, y: x.eval(context) or y.eval(context)),
  77. 'and': infix(7, lambda context, x, y: x.eval(context) and y.eval(context)),
  78. 'not': prefix(8, lambda context, x: not x.eval(context)),
  79. 'in': infix(9, lambda context, x, y: x.eval(context) in y.eval(context)),
  80. 'not in': infix(9, lambda context, x, y: x.eval(context) not in y.eval(context)),
  81. 'is': infix(10, lambda context, x, y: x.eval(context) is y.eval(context)),
  82. 'is not': infix(10, lambda context, x, y: x.eval(context) is not 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. }
  90. # Assign 'id' to each:
  91. for key, op in OPERATORS.items():
  92. op.id = key
  93. class Literal(TokenBase):
  94. """
  95. A basic self-resolvable object similar to a Django template variable.
  96. """
  97. # IfParser uses Literal in create_var, but TemplateIfParser overrides
  98. # create_var so that a proper implementation that actually resolves
  99. # variables, filters etc. is used.
  100. id = "literal"
  101. lbp = 0
  102. def __init__(self, value):
  103. self.value = value
  104. def display(self):
  105. return repr(self.value)
  106. def nud(self, parser):
  107. return self
  108. def eval(self, context):
  109. return self.value
  110. def __repr__(self):
  111. return "(%s %r)" % (self.id, self.value)
  112. class EndToken(TokenBase):
  113. lbp = 0
  114. def nud(self, parser):
  115. raise parser.error_class("Unexpected end of expression in if tag.")
  116. EndToken = EndToken()
  117. class IfParser:
  118. error_class = ValueError
  119. def __init__(self, tokens):
  120. # Turn 'is','not' and 'not','in' into single tokens.
  121. num_tokens = len(tokens)
  122. mapped_tokens = []
  123. i = 0
  124. while i < num_tokens:
  125. token = tokens[i]
  126. if token == "is" and i + 1 < num_tokens and tokens[i + 1] == "not":
  127. token = "is not"
  128. i += 1 # skip 'not'
  129. elif token == "not" and i + 1 < num_tokens and tokens[i + 1] == "in":
  130. token = "not in"
  131. i += 1 # skip 'in'
  132. mapped_tokens.append(self.translate_token(token))
  133. i += 1
  134. self.tokens = mapped_tokens
  135. self.pos = 0
  136. self.current_token = self.next_token()
  137. def translate_token(self, token):
  138. try:
  139. op = OPERATORS[token]
  140. except (KeyError, TypeError):
  141. return self.create_var(token)
  142. else:
  143. return op()
  144. def next_token(self):
  145. if self.pos >= len(self.tokens):
  146. return EndToken
  147. else:
  148. retval = self.tokens[self.pos]
  149. self.pos += 1
  150. return retval
  151. def parse(self):
  152. retval = self.expression()
  153. # Check that we have exhausted all the tokens
  154. if self.current_token is not EndToken:
  155. raise self.error_class("Unused '%s' at end of if expression." %
  156. self.current_token.display())
  157. return retval
  158. def expression(self, rbp=0):
  159. t = self.current_token
  160. self.current_token = self.next_token()
  161. left = t.nud(self)
  162. while rbp < self.current_token.lbp:
  163. t = self.current_token
  164. self.current_token = self.next_token()
  165. left = t.led(left, self)
  166. return left
  167. def create_var(self, value):
  168. return Literal(value)