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&lt;script&gt;"
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>") == "&lt;foo&gt;"
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