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