1import os 2import shutil 3import tempfile 4 5import pytest 6 7from jinja2 import ChainableUndefined 8from jinja2 import DebugUndefined 9from jinja2 import DictLoader 10from jinja2 import Environment 11from jinja2 import is_undefined 12from jinja2 import make_logging_undefined 13from jinja2 import meta 14from jinja2 import StrictUndefined 15from jinja2 import Template 16from jinja2 import TemplatesNotFound 17from jinja2 import Undefined 18from jinja2 import UndefinedError 19from jinja2.compiler import CodeGenerator 20from jinja2.runtime import Context 21from jinja2.utils import contextfunction 22from jinja2.utils import Cycler 23from jinja2.utils import environmentfunction 24from jinja2.utils import evalcontextfunction 25 26 27class TestExtendedAPI: 28 def test_item_and_attribute(self, env): 29 from jinja2.sandbox import SandboxedEnvironment 30 31 for env in Environment(), SandboxedEnvironment(): 32 tmpl = env.from_string("{{ foo.items()|list }}") 33 assert tmpl.render(foo={"items": 42}) == "[('items', 42)]" 34 tmpl = env.from_string('{{ foo|attr("items")()|list }}') 35 assert tmpl.render(foo={"items": 42}) == "[('items', 42)]" 36 tmpl = env.from_string('{{ foo["items"] }}') 37 assert tmpl.render(foo={"items": 42}) == "42" 38 39 def test_finalize(self): 40 e = Environment(finalize=lambda v: "" if v is None else v) 41 t = e.from_string("{% for item in seq %}|{{ item }}{% endfor %}") 42 assert t.render(seq=(None, 1, "foo")) == "||1|foo" 43 44 def test_finalize_constant_expression(self): 45 e = Environment(finalize=lambda v: "" if v is None else v) 46 t = e.from_string("<{{ none }}>") 47 assert t.render() == "<>" 48 49 def test_no_finalize_template_data(self): 50 e = Environment(finalize=lambda v: type(v).__name__) 51 t = e.from_string("<{{ value }}>") 52 # If template data was finalized, it would print "strintstr". 53 assert t.render(value=123) == "<int>" 54 55 def test_context_finalize(self): 56 @contextfunction 57 def finalize(context, value): 58 return value * context["scale"] 59 60 e = Environment(finalize=finalize) 61 t = e.from_string("{{ value }}") 62 assert t.render(value=5, scale=3) == "15" 63 64 def test_eval_finalize(self): 65 @evalcontextfunction 66 def finalize(eval_ctx, value): 67 return str(eval_ctx.autoescape) + value 68 69 e = Environment(finalize=finalize, autoescape=True) 70 t = e.from_string("{{ value }}") 71 assert t.render(value="<script>") == "True<script>" 72 73 def test_env_autoescape(self): 74 @environmentfunction 75 def finalize(env, value): 76 return " ".join( 77 (env.variable_start_string, repr(value), env.variable_end_string) 78 ) 79 80 e = Environment(finalize=finalize) 81 t = e.from_string("{{ value }}") 82 assert t.render(value="hello") == "{{ 'hello' }}" 83 84 def test_cycler(self, env): 85 items = 1, 2, 3 86 c = Cycler(*items) 87 for item in items + items: 88 assert c.current == item 89 assert next(c) == item 90 next(c) 91 assert c.current == 2 92 c.reset() 93 assert c.current == 1 94 95 def test_expressions(self, env): 96 expr = env.compile_expression("foo") 97 assert expr() is None 98 assert expr(foo=42) == 42 99 expr2 = env.compile_expression("foo", undefined_to_none=False) 100 assert is_undefined(expr2()) 101 102 expr = env.compile_expression("42 + foo") 103 assert expr(foo=42) == 84 104 105 def test_template_passthrough(self, env): 106 t = Template("Content") 107 assert env.get_template(t) is t 108 assert env.select_template([t]) is t 109 assert env.get_or_select_template([t]) is t 110 assert env.get_or_select_template(t) is t 111 112 def test_get_template_undefined(self, env): 113 """Passing Undefined to get/select_template raises an 114 UndefinedError or shows the undefined message in the list. 115 """ 116 env.loader = DictLoader({}) 117 t = Undefined(name="no_name_1") 118 119 with pytest.raises(UndefinedError): 120 env.get_template(t) 121 122 with pytest.raises(UndefinedError): 123 env.get_or_select_template(t) 124 125 with pytest.raises(UndefinedError): 126 env.select_template(t) 127 128 with pytest.raises(TemplatesNotFound) as exc_info: 129 env.select_template([t, "no_name_2"]) 130 131 exc_message = str(exc_info.value) 132 assert "'no_name_1' is undefined" in exc_message 133 assert "no_name_2" in exc_message 134 135 def test_autoescape_autoselect(self, env): 136 def select_autoescape(name): 137 if name is None or "." not in name: 138 return False 139 return name.endswith(".html") 140 141 env = Environment( 142 autoescape=select_autoescape, 143 loader=DictLoader({"test.txt": "{{ foo }}", "test.html": "{{ foo }}"}), 144 ) 145 t = env.get_template("test.txt") 146 assert t.render(foo="<foo>") == "<foo>" 147 t = env.get_template("test.html") 148 assert t.render(foo="<foo>") == "<foo>" 149 t = env.from_string("{{ foo }}") 150 assert t.render(foo="<foo>") == "<foo>" 151 152 def test_sandbox_max_range(self, env): 153 from jinja2.sandbox import SandboxedEnvironment, MAX_RANGE 154 155 env = SandboxedEnvironment() 156 t = env.from_string("{% for item in range(total) %}{{ item }}{% endfor %}") 157 158 with pytest.raises(OverflowError): 159 t.render(total=MAX_RANGE + 1) 160 161 162class TestMeta: 163 def test_find_undeclared_variables(self, env): 164 ast = env.parse("{% set foo = 42 %}{{ bar + foo }}") 165 x = meta.find_undeclared_variables(ast) 166 assert x == {"bar"} 167 168 ast = env.parse( 169 "{% set foo = 42 %}{{ bar + foo }}" 170 "{% macro meh(x) %}{{ x }}{% endmacro %}" 171 "{% for item in seq %}{{ muh(item) + meh(seq) }}" 172 "{% endfor %}" 173 ) 174 x = meta.find_undeclared_variables(ast) 175 assert x == {"bar", "seq", "muh"} 176 177 ast = env.parse("{% for x in range(5) %}{{ x }}{% endfor %}{{ foo }}") 178 x = meta.find_undeclared_variables(ast) 179 assert x == {"foo"} 180 181 def test_find_refererenced_templates(self, env): 182 ast = env.parse('{% extends "layout.html" %}{% include helper %}') 183 i = meta.find_referenced_templates(ast) 184 assert next(i) == "layout.html" 185 assert next(i) is None 186 assert list(i) == [] 187 188 ast = env.parse( 189 '{% extends "layout.html" %}' 190 '{% from "test.html" import a, b as c %}' 191 '{% import "meh.html" as meh %}' 192 '{% include "muh.html" %}' 193 ) 194 i = meta.find_referenced_templates(ast) 195 assert list(i) == ["layout.html", "test.html", "meh.html", "muh.html"] 196 197 def test_find_included_templates(self, env): 198 ast = env.parse('{% include ["foo.html", "bar.html"] %}') 199 i = meta.find_referenced_templates(ast) 200 assert list(i) == ["foo.html", "bar.html"] 201 202 ast = env.parse('{% include ("foo.html", "bar.html") %}') 203 i = meta.find_referenced_templates(ast) 204 assert list(i) == ["foo.html", "bar.html"] 205 206 ast = env.parse('{% include ["foo.html", "bar.html", foo] %}') 207 i = meta.find_referenced_templates(ast) 208 assert list(i) == ["foo.html", "bar.html", None] 209 210 ast = env.parse('{% include ("foo.html", "bar.html", foo) %}') 211 i = meta.find_referenced_templates(ast) 212 assert list(i) == ["foo.html", "bar.html", None] 213 214 215class TestStreaming: 216 def test_basic_streaming(self, env): 217 t = env.from_string( 218 "<ul>{% for item in seq %}<li>{{ loop.index }} - {{ item }}</li>" 219 "{%- endfor %}</ul>" 220 ) 221 stream = t.stream(seq=list(range(3))) 222 assert next(stream) == "<ul>" 223 assert "".join(stream) == "<li>1 - 0</li><li>2 - 1</li><li>3 - 2</li></ul>" 224 225 def test_buffered_streaming(self, env): 226 tmpl = env.from_string( 227 "<ul>{% for item in seq %}<li>{{ loop.index }} - {{ item }}</li>" 228 "{%- endfor %}</ul>" 229 ) 230 stream = tmpl.stream(seq=list(range(3))) 231 stream.enable_buffering(size=3) 232 assert next(stream) == "<ul><li>1" 233 assert next(stream) == " - 0</li>" 234 235 def test_streaming_behavior(self, env): 236 tmpl = env.from_string("") 237 stream = tmpl.stream() 238 assert not stream.buffered 239 stream.enable_buffering(20) 240 assert stream.buffered 241 stream.disable_buffering() 242 assert not stream.buffered 243 244 def test_dump_stream(self, env): 245 tmp = tempfile.mkdtemp() 246 try: 247 tmpl = env.from_string("\u2713") 248 stream = tmpl.stream() 249 stream.dump(os.path.join(tmp, "dump.txt"), "utf-8") 250 with open(os.path.join(tmp, "dump.txt"), "rb") as f: 251 assert f.read() == b"\xe2\x9c\x93" 252 finally: 253 shutil.rmtree(tmp) 254 255 256class TestUndefined: 257 def test_stopiteration_is_undefined(self): 258 def test(): 259 raise StopIteration() 260 261 t = Template("A{{ test() }}B") 262 assert t.render(test=test) == "AB" 263 t = Template("A{{ test().missingattribute }}B") 264 pytest.raises(UndefinedError, t.render, test=test) 265 266 def test_undefined_and_special_attributes(self): 267 with pytest.raises(AttributeError): 268 Undefined("Foo").__dict__ 269 270 def test_undefined_attribute_error(self): 271 # Django's LazyObject turns the __class__ attribute into a 272 # property that resolves the wrapped function. If that wrapped 273 # function raises an AttributeError, printing the repr of the 274 # object in the undefined message would cause a RecursionError. 275 class Error: 276 @property 277 def __class__(self): 278 raise AttributeError() 279 280 u = Undefined(obj=Error(), name="hello") 281 282 with pytest.raises(UndefinedError): 283 getattr(u, "recursion", None) 284 285 def test_logging_undefined(self): 286 _messages = [] 287 288 class DebugLogger: 289 def warning(self, msg, *args): 290 _messages.append("W:" + msg % args) 291 292 def error(self, msg, *args): 293 _messages.append("E:" + msg % args) 294 295 logging_undefined = make_logging_undefined(DebugLogger()) 296 env = Environment(undefined=logging_undefined) 297 assert env.from_string("{{ missing }}").render() == "" 298 pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render) 299 assert env.from_string("{{ missing|list }}").render() == "[]" 300 assert env.from_string("{{ missing is not defined }}").render() == "True" 301 assert env.from_string("{{ foo.missing }}").render(foo=42) == "" 302 assert env.from_string("{{ not missing }}").render() == "True" 303 assert _messages == [ 304 "W:Template variable warning: 'missing' is undefined", 305 "E:Template variable error: 'missing' is undefined", 306 "W:Template variable warning: 'missing' is undefined", 307 "W:Template variable warning: 'int object' has no attribute 'missing'", 308 "W:Template variable warning: 'missing' is undefined", 309 ] 310 311 def test_default_undefined(self): 312 env = Environment(undefined=Undefined) 313 assert env.from_string("{{ missing }}").render() == "" 314 pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render) 315 assert env.from_string("{{ missing|list }}").render() == "[]" 316 assert env.from_string("{{ missing is not defined }}").render() == "True" 317 assert env.from_string("{{ foo.missing }}").render(foo=42) == "" 318 assert env.from_string("{{ not missing }}").render() == "True" 319 pytest.raises(UndefinedError, env.from_string("{{ missing - 1}}").render) 320 und1 = Undefined(name="x") 321 und2 = Undefined(name="y") 322 assert und1 == und2 323 assert und1 != 42 324 assert hash(und1) == hash(und2) == hash(Undefined()) 325 with pytest.raises(AttributeError): 326 getattr(Undefined, "__slots__") # noqa: B009 327 328 def test_chainable_undefined(self): 329 env = Environment(undefined=ChainableUndefined) 330 # The following tests are copied from test_default_undefined 331 assert env.from_string("{{ missing }}").render() == "" 332 assert env.from_string("{{ missing|list }}").render() == "[]" 333 assert env.from_string("{{ missing is not defined }}").render() == "True" 334 assert env.from_string("{{ foo.missing }}").render(foo=42) == "" 335 assert env.from_string("{{ not missing }}").render() == "True" 336 pytest.raises(UndefinedError, env.from_string("{{ missing - 1}}").render) 337 with pytest.raises(AttributeError): 338 getattr(ChainableUndefined, "__slots__") # noqa: B009 339 340 # The following tests ensure subclass functionality works as expected 341 assert env.from_string('{{ missing.bar["baz"] }}').render() == "" 342 assert env.from_string('{{ foo.bar["baz"]._undefined_name }}').render() == "foo" 343 assert ( 344 env.from_string('{{ foo.bar["baz"]._undefined_name }}').render(foo=42) 345 == "bar" 346 ) 347 assert ( 348 env.from_string('{{ foo.bar["baz"]._undefined_name }}').render( 349 foo={"bar": 42} 350 ) 351 == "baz" 352 ) 353 354 def test_debug_undefined(self): 355 env = Environment(undefined=DebugUndefined) 356 assert env.from_string("{{ missing }}").render() == "{{ missing }}" 357 pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render) 358 assert env.from_string("{{ missing|list }}").render() == "[]" 359 assert env.from_string("{{ missing is not defined }}").render() == "True" 360 assert ( 361 env.from_string("{{ foo.missing }}").render(foo=42) 362 == "{{ no such element: int object['missing'] }}" 363 ) 364 assert env.from_string("{{ not missing }}").render() == "True" 365 undefined_hint = "this is testing undefined hint of DebugUndefined" 366 assert ( 367 str(DebugUndefined(hint=undefined_hint)) 368 == f"{{{{ undefined value printed: {undefined_hint} }}}}" 369 ) 370 with pytest.raises(AttributeError): 371 getattr(DebugUndefined, "__slots__") # noqa: B009 372 373 def test_strict_undefined(self): 374 env = Environment(undefined=StrictUndefined) 375 pytest.raises(UndefinedError, env.from_string("{{ missing }}").render) 376 pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render) 377 pytest.raises(UndefinedError, env.from_string("{{ missing|list }}").render) 378 assert env.from_string("{{ missing is not defined }}").render() == "True" 379 pytest.raises( 380 UndefinedError, env.from_string("{{ foo.missing }}").render, foo=42 381 ) 382 pytest.raises(UndefinedError, env.from_string("{{ not missing }}").render) 383 assert ( 384 env.from_string('{{ missing|default("default", true) }}').render() 385 == "default" 386 ) 387 with pytest.raises(AttributeError): 388 getattr(StrictUndefined, "__slots__") # noqa: B009 389 assert env.from_string('{{ "foo" if false }}').render() == "" 390 391 def test_indexing_gives_undefined(self): 392 t = Template("{{ var[42].foo }}") 393 pytest.raises(UndefinedError, t.render, var=0) 394 395 def test_none_gives_proper_error(self): 396 with pytest.raises(UndefinedError, match="'None' has no attribute 'split'"): 397 Environment().getattr(None, "split")() 398 399 def test_object_repr(self): 400 with pytest.raises( 401 UndefinedError, match="'int object' has no attribute 'upper'" 402 ): 403 Undefined(obj=42, name="upper")() 404 405 406class TestLowLevel: 407 def test_custom_code_generator(self): 408 class CustomCodeGenerator(CodeGenerator): 409 def visit_Const(self, node, frame=None): 410 # This method is pure nonsense, but works fine for testing... 411 if node.value == "foo": 412 self.write(repr("bar")) 413 else: 414 super().visit_Const(node, frame) 415 416 class CustomEnvironment(Environment): 417 code_generator_class = CustomCodeGenerator 418 419 env = CustomEnvironment() 420 tmpl = env.from_string('{% set foo = "foo" %}{{ foo }}') 421 assert tmpl.render() == "bar" 422 423 def test_custom_context(self): 424 class CustomContext(Context): 425 def resolve_or_missing(self, key): 426 return "resolve-" + key 427 428 class CustomEnvironment(Environment): 429 context_class = CustomContext 430 431 env = CustomEnvironment() 432 tmpl = env.from_string("{{ foo }}") 433 assert tmpl.render() == "resolve-foo" 434