diff --git a/CHANGES b/CHANGES index ecc9430d7f13e941a41c5c6fee0612df2be45ce0..d25f1b9ca75846b1234e7eb6e8e5cb4ff809e013 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,20 @@ variables not found in the context to raise a NameError immediately, instead of defaulting to the UNDEFINED value. + +- The range of Python identifiers that + are considered "undefined", meaning they + are pulled from the context, has been + trimmed back to not include variables + declared inside of expressions (i.e. from + list comprehensions), as well as + in the argument list of lambdas. This + to better support the strict_undefined + feature. The change should be + fully backwards-compatible but involved + a little bit of tinkering in the AST code, + which hadn't really been touched for + a couple of years, just FYI. 0.3.5 - The <%namespace> tag allows expressions diff --git a/mako/parsetree.py b/mako/parsetree.py index 12b0498349dabc95a69b97bd8e81dcb817590eab..554531e2bd4061a09d3ba515edaa4e2cbbfd6e7d 100644 --- a/mako/parsetree.py +++ b/mako/parsetree.py @@ -176,7 +176,7 @@ class Expression(Node): self.escapes_code.undeclared_identifiers.difference( set(filters.DEFAULT_ESCAPES.keys()) ) - ) + ).difference(self.code.declared_identifiers) def __repr__(self): return "Expression(%r, %r, %r)" % ( @@ -274,6 +274,10 @@ class Tag(Node): if m: code = ast.PythonCode(m.group(1).rstrip(), **self.exception_kwargs) + # we aren't discarding "declared_identifiers" here, + # which we do so that list comprehension-declared variables + # aren't counted. As yet can't find a condition that + # requires it here. undeclared_identifiers = \ undeclared_identifiers.union( code.undeclared_identifiers) @@ -327,7 +331,9 @@ class IncludeTag(Tag): return [] def undeclared_identifiers(self): - identifiers = self.page_args.undeclared_identifiers.difference(set(["__DUMMY"])) + identifiers = self.page_args.undeclared_identifiers.\ + difference(set(["__DUMMY"])).\ + difference(self.page_args.declared_identifiers) return identifiers.union(super(IncludeTag, self).undeclared_identifiers()) class NamespaceTag(Tag): @@ -414,7 +420,8 @@ class CallTag(Tag): return self.code.declared_identifiers.union(self.body_decl.argnames) def undeclared_identifiers(self): - return self.code.undeclared_identifiers + return self.code.undeclared_identifiers.\ + difference(self.code.declared_identifiers) class CallNamespaceTag(Tag): @@ -443,7 +450,8 @@ class CallNamespaceTag(Tag): return self.code.declared_identifiers.union(self.body_decl.argnames) def undeclared_identifiers(self): - return self.code.undeclared_identifiers + return self.code.undeclared_identifiers.\ + difference(self.code.declared_identifiers) class InheritTag(Tag): __keyword__ = 'inherit' diff --git a/mako/pyparser.py b/mako/pyparser.py index b90278e2e96dd1eb8beb6ce4b349225d51f07463..d011690424174577197ce5e7abab2b4927330b12 100644 --- a/mako/pyparser.py +++ b/mako/pyparser.py @@ -89,12 +89,19 @@ if _ast: for statement in node.body: self.visit(statement) + def visit_Lambda(self, node, *args): + self._visit_function(node, True) + def visit_FunctionDef(self, node): self._add_declared(node.name) + self._visit_function(node, False) + + def _visit_function(self, node, islambda): # push function state onto stack. dont log any # more identifiers as "declared" until outside of the function, # but keep logging identifiers as "undeclared". - # track argument names in each function header so they arent counted as "undeclared" + # track argument names in each function header + # so they arent counted as "undeclared" saved = {} inf = self.in_function self.in_function = True @@ -104,13 +111,16 @@ if _ast: saved[arg_id(arg)] = True else: self.local_ident_stack[arg_id(arg)] = True - for n in node.body: - self.visit(n) + if islambda: + self.visit(node.body) + else: + for n in node.body: + self.visit(n) self.in_function = inf for arg in node.args.args: if arg_id(arg) not in saved: del self.local_ident_stack[arg_id(arg)] - + def visit_For(self, node): # flip around visit self.visit(node.iter) @@ -138,7 +148,13 @@ if _ast: self._add_declared(name.asname) else: if name.name == '*': - raise exceptions.CompileException("'import *' is not supported, since all identifier names must be explicitly declared. Please use the form 'from <modulename> import <name1>, <name2>, ...' instead.", **self.exception_kwargs) + raise exceptions.CompileException( + "'import *' is not supported, since all " + "identifier names must be explicitly " + "declared. Please use the form 'from " + "<modulename> import <name1>, " + "<name2>, ...' instead.", + **self.exception_kwargs) self._add_declared(name.name) class FindTuple(_ast_util.NodeVisitor): @@ -197,12 +213,17 @@ else: self.visit(node.expr, *args) for n in node.nodes: self.visit(n, *args) + def visitLambda(self, node, *args): + self._visit_function(node, args) def visitFunction(self,node, *args): self._add_declared(node.name) + self._visit_function(node, args) + def _visit_function(self, node, args): # push function state onto stack. dont log any # more identifiers as "declared" until outside of the function, # but keep logging identifiers as "undeclared". - # track argument names in each function header so they arent counted as "undeclared" + # track argument names in each function header so + # they arent counted as "undeclared" saved = {} inf = self.in_function self.in_function = True @@ -217,6 +238,7 @@ else: for arg in node.argnames: if arg not in saved: del self.local_ident_stack[arg] + def visitFor(self, node, *args): # flip around visit self.visit(node.list, *args) diff --git a/test/test_ast.py b/test/test_ast.py index bfdfd90e2b5250a23ad7be57394c1dd4a1400e3a..b9fe9487e93d255383bd2ecc7e5aee5c8221ee6c 100644 --- a/test/test_ast.py +++ b/test/test_ast.py @@ -156,7 +156,39 @@ class Hi(object): parsed = ast.PythonCode(code, **exception_kwargs) assert parsed.declared_identifiers == set(['Hi']) assert parsed.undeclared_identifiers == set() + + def test_locate_identifiers_9(self): + code = """ + ",".join([t for t in ("a", "b", "c")]) +""" + parsed = ast.PythonCode(code, **exception_kwargs) + assert parsed.declared_identifiers == set(['t']) + assert parsed.undeclared_identifiers == set(['t']) + + code = """ + [(val, name) for val, name in x] +""" + parsed = ast.PythonCode(code, **exception_kwargs) + assert parsed.declared_identifiers == set(['val', 'name']) + assert parsed.undeclared_identifiers == set(['val', 'name', 'x']) + def test_locate_identifiers_10(self): + code = """ +lambda q: q + 5 +""" + parsed = ast.PythonCode(code, **exception_kwargs) + eq_(parsed.declared_identifiers, set()) + eq_(parsed.undeclared_identifiers, set()) + + def test_locate_identifiers_11(self): + code = """ +def x(q): + return q + 5 +""" + parsed = ast.PythonCode(code, **exception_kwargs) + eq_(parsed.declared_identifiers, set(['x'])) + eq_(parsed.undeclared_identifiers, set()) + def test_no_global_imports(self): code = """ from foo import * diff --git a/test/test_template.py b/test/test_template.py index ddf746a34d2093dcf78c94db5d933ff872166e41..dbfd068162a7f2ca3f754d7668b220455c720cdc 100644 --- a/test/test_template.py +++ b/test/test_template.py @@ -591,6 +591,104 @@ class UndefinedVarsTest(TemplateTest): t.render, y=12 ) + def test_expression_declared(self): + t = Template(""" + ${",".join([t for t in ("a", "b", "c")])} + """, strict_undefined=True) + + eq_(result_lines(t.render()), ['a,b,c']) + + t = Template(""" + <%self:foo value="${[(val, n) for val, n in [(1, 2)]]}"/> + + <%def name="foo(value)"> + ${value} + </%def> + + """, strict_undefined=True) + + eq_(result_lines(t.render()), ['[(1, 2)]']) + + t = Template(""" + <%call expr="foo(value=[(val, n) for val, n in [(1, 2)]])" /> + + <%def name="foo(value)"> + ${value} + </%def> + + """, strict_undefined=True) + + eq_(result_lines(t.render()), ['[(1, 2)]']) + + l = TemplateLookup(strict_undefined=True) + l.put_string("i", "hi, ${pageargs['y']}") + l.put_string("t", """ + <%include file="i" args="y=[x for x in range(3)]" /> + """) + eq_( + result_lines(l.get_template("t").render()), ['hi, [0, 1, 2]'] + ) + + l.put_string('q', """ + <%namespace name="i" file="${(str([x for x in range(3)][2]) + 'i')[-1]}" /> + ${i.body(y='x')} + """) + eq_( + result_lines(l.get_template("q").render()), ['hi, x'] + ) + + t = Template(""" + <% + y = lambda q: str(q) + %> + ${y('hi')} + """, strict_undefined=True) + eq_( + result_lines(t.render()), ["hi"] + ) + + def test_list_comprehensions_plus_undeclared_nonstrict(self): + # traditional behavior. variable inside a list comprehension + # is treated as an "undefined", so is pulled from the context. + t = Template(""" + t is: ${t} + + ${",".join([t for t in ("a", "b", "c")])} + """) + + eq_( + result_lines(t.render(t="T")), + ['t is: T', 'a,b,c'] + ) + + def test_traditional_assignment_plus_undeclared(self): + t = Template(""" + t is: ${t} + + <% + t = 12 + %> + """) + assert_raises( + UnboundLocalError, + t.render, t="T" + ) + + def test_list_comprehensions_plus_undeclared_strict(self): + # with strict, a list comprehension now behaves + # like the undeclared case above. + t = Template(""" + t is: ${t} + + ${",".join([t for t in ("a", "b", "c")])} + """, strict_undefined=True) + + eq_( + result_lines(t.render(t="T")), + ['t is: T', 'a,b,c'] + ) + + class ControlTest(TemplateTest): def test_control(self): t = Template("""