• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import importlib.abc
2import importlib.machinery
3import importlib.util
4import os
5import platform
6import shutil
7import sys
8import tempfile
9import time
10import weakref
11
12import pytest
13
14from jinja2 import Environment
15from jinja2 import loaders
16from jinja2 import PackageLoader
17from jinja2.exceptions import TemplateNotFound
18from jinja2.loaders import split_template_path
19
20
21class TestLoaders:
22    def test_dict_loader(self, dict_loader):
23        env = Environment(loader=dict_loader)
24        tmpl = env.get_template("justdict.html")
25        assert tmpl.render().strip() == "FOO"
26        pytest.raises(TemplateNotFound, env.get_template, "missing.html")
27
28    def test_package_loader(self, package_loader):
29        env = Environment(loader=package_loader)
30        tmpl = env.get_template("test.html")
31        assert tmpl.render().strip() == "BAR"
32        pytest.raises(TemplateNotFound, env.get_template, "missing.html")
33
34    def test_filesystem_loader_overlapping_names(self, filesystem_loader):
35        res = os.path.dirname(filesystem_loader.searchpath[0])
36        t2_dir = os.path.join(res, "templates2")
37        # Make "foo" show up before "foo/test.html".
38        filesystem_loader.searchpath.insert(0, t2_dir)
39        e = Environment(loader=filesystem_loader)
40        e.get_template("foo")
41        # This would raise NotADirectoryError if "t2/foo" wasn't skipped.
42        e.get_template("foo/test.html")
43
44    def test_choice_loader(self, choice_loader):
45        env = Environment(loader=choice_loader)
46        tmpl = env.get_template("justdict.html")
47        assert tmpl.render().strip() == "FOO"
48        tmpl = env.get_template("test.html")
49        assert tmpl.render().strip() == "BAR"
50        pytest.raises(TemplateNotFound, env.get_template, "missing.html")
51
52    def test_function_loader(self, function_loader):
53        env = Environment(loader=function_loader)
54        tmpl = env.get_template("justfunction.html")
55        assert tmpl.render().strip() == "FOO"
56        pytest.raises(TemplateNotFound, env.get_template, "missing.html")
57
58    def test_prefix_loader(self, prefix_loader):
59        env = Environment(loader=prefix_loader)
60        tmpl = env.get_template("a/test.html")
61        assert tmpl.render().strip() == "BAR"
62        tmpl = env.get_template("b/justdict.html")
63        assert tmpl.render().strip() == "FOO"
64        pytest.raises(TemplateNotFound, env.get_template, "missing")
65
66    def test_caching(self):
67        changed = False
68
69        class TestLoader(loaders.BaseLoader):
70            def get_source(self, environment, template):
71                return "foo", None, lambda: not changed
72
73        env = Environment(loader=TestLoader(), cache_size=-1)
74        tmpl = env.get_template("template")
75        assert tmpl is env.get_template("template")
76        changed = True
77        assert tmpl is not env.get_template("template")
78        changed = False
79
80    def test_no_cache(self):
81        mapping = {"foo": "one"}
82        env = Environment(loader=loaders.DictLoader(mapping), cache_size=0)
83        assert env.get_template("foo") is not env.get_template("foo")
84
85    def test_limited_size_cache(self):
86        mapping = {"one": "foo", "two": "bar", "three": "baz"}
87        loader = loaders.DictLoader(mapping)
88        env = Environment(loader=loader, cache_size=2)
89        t1 = env.get_template("one")
90        t2 = env.get_template("two")
91        assert t2 is env.get_template("two")
92        assert t1 is env.get_template("one")
93        env.get_template("three")
94        loader_ref = weakref.ref(loader)
95        assert (loader_ref, "one") in env.cache
96        assert (loader_ref, "two") not in env.cache
97        assert (loader_ref, "three") in env.cache
98
99    def test_cache_loader_change(self):
100        loader1 = loaders.DictLoader({"foo": "one"})
101        loader2 = loaders.DictLoader({"foo": "two"})
102        env = Environment(loader=loader1, cache_size=2)
103        assert env.get_template("foo").render() == "one"
104        env.loader = loader2
105        assert env.get_template("foo").render() == "two"
106
107    def test_dict_loader_cache_invalidates(self):
108        mapping = {"foo": "one"}
109        env = Environment(loader=loaders.DictLoader(mapping))
110        assert env.get_template("foo").render() == "one"
111        mapping["foo"] = "two"
112        assert env.get_template("foo").render() == "two"
113
114    def test_split_template_path(self):
115        assert split_template_path("foo/bar") == ["foo", "bar"]
116        assert split_template_path("./foo/bar") == ["foo", "bar"]
117        pytest.raises(TemplateNotFound, split_template_path, "../foo")
118
119
120class TestFileSystemLoader:
121    searchpath = os.path.join(
122        os.path.dirname(os.path.abspath(__file__)), "res", "templates"
123    )
124
125    @staticmethod
126    def _test_common(env):
127        tmpl = env.get_template("test.html")
128        assert tmpl.render().strip() == "BAR"
129        tmpl = env.get_template("foo/test.html")
130        assert tmpl.render().strip() == "FOO"
131        pytest.raises(TemplateNotFound, env.get_template, "missing.html")
132
133    def test_searchpath_as_str(self):
134        filesystem_loader = loaders.FileSystemLoader(self.searchpath)
135
136        env = Environment(loader=filesystem_loader)
137        self._test_common(env)
138
139    def test_searchpath_as_pathlib(self):
140        import pathlib
141
142        searchpath = pathlib.Path(self.searchpath)
143        filesystem_loader = loaders.FileSystemLoader(searchpath)
144        env = Environment(loader=filesystem_loader)
145        self._test_common(env)
146
147    def test_searchpath_as_list_including_pathlib(self):
148        import pathlib
149
150        searchpath = pathlib.Path(self.searchpath)
151        filesystem_loader = loaders.FileSystemLoader(["/tmp/templates", searchpath])
152        env = Environment(loader=filesystem_loader)
153        self._test_common(env)
154
155    def test_caches_template_based_on_mtime(self):
156        filesystem_loader = loaders.FileSystemLoader(self.searchpath)
157
158        env = Environment(loader=filesystem_loader)
159        tmpl1 = env.get_template("test.html")
160        tmpl2 = env.get_template("test.html")
161        assert tmpl1 is tmpl2
162
163        os.utime(os.path.join(self.searchpath, "test.html"), (time.time(), time.time()))
164        tmpl3 = env.get_template("test.html")
165        assert tmpl1 is not tmpl3
166
167    @pytest.mark.parametrize(
168        ("encoding", "expect"),
169        [
170            ("utf-8", "文字化け"),
171            ("iso-8859-1", "æ\x96\x87\xe5\xad\x97\xe5\x8c\x96\xe3\x81\x91"),
172        ],
173    )
174    def test_uses_specified_encoding(self, encoding, expect):
175        loader = loaders.FileSystemLoader(self.searchpath, encoding=encoding)
176        e = Environment(loader=loader)
177        t = e.get_template("mojibake.txt")
178        assert t.render() == expect
179
180
181class TestModuleLoader:
182    archive = None
183
184    def compile_down(self, prefix_loader, zip="deflated"):
185        log = []
186        self.reg_env = Environment(loader=prefix_loader)
187        if zip is not None:
188            fd, self.archive = tempfile.mkstemp(suffix=".zip")
189            os.close(fd)
190        else:
191            self.archive = tempfile.mkdtemp()
192        self.reg_env.compile_templates(self.archive, zip=zip, log_function=log.append)
193        self.mod_env = Environment(loader=loaders.ModuleLoader(self.archive))
194        return "".join(log)
195
196    def teardown(self):
197        if hasattr(self, "mod_env"):
198            if os.path.isfile(self.archive):
199                os.remove(self.archive)
200            else:
201                shutil.rmtree(self.archive)
202            self.archive = None
203
204    def test_log(self, prefix_loader):
205        log = self.compile_down(prefix_loader)
206        assert (
207            'Compiled "a/foo/test.html" as '
208            "tmpl_a790caf9d669e39ea4d280d597ec891c4ef0404a" in log
209        )
210        assert "Finished compiling templates" in log
211        assert (
212            'Could not compile "a/syntaxerror.html": '
213            "Encountered unknown tag 'endif'" in log
214        )
215
216    def _test_common(self):
217        tmpl1 = self.reg_env.get_template("a/test.html")
218        tmpl2 = self.mod_env.get_template("a/test.html")
219        assert tmpl1.render() == tmpl2.render()
220
221        tmpl1 = self.reg_env.get_template("b/justdict.html")
222        tmpl2 = self.mod_env.get_template("b/justdict.html")
223        assert tmpl1.render() == tmpl2.render()
224
225    def test_deflated_zip_compile(self, prefix_loader):
226        self.compile_down(prefix_loader, zip="deflated")
227        self._test_common()
228
229    def test_stored_zip_compile(self, prefix_loader):
230        self.compile_down(prefix_loader, zip="stored")
231        self._test_common()
232
233    def test_filesystem_compile(self, prefix_loader):
234        self.compile_down(prefix_loader, zip=None)
235        self._test_common()
236
237    def test_weak_references(self, prefix_loader):
238        self.compile_down(prefix_loader)
239        self.mod_env.get_template("a/test.html")
240        key = loaders.ModuleLoader.get_template_key("a/test.html")
241        name = self.mod_env.loader.module.__name__
242
243        assert hasattr(self.mod_env.loader.module, key)
244        assert name in sys.modules
245
246        # unset all, ensure the module is gone from sys.modules
247        self.mod_env = None
248
249        try:
250            import gc
251
252            gc.collect()
253        except BaseException:
254            pass
255
256        assert name not in sys.modules
257
258    def test_choice_loader(self, prefix_loader):
259        self.compile_down(prefix_loader)
260        self.mod_env.loader = loaders.ChoiceLoader(
261            [self.mod_env.loader, loaders.DictLoader({"DICT_SOURCE": "DICT_TEMPLATE"})]
262        )
263        tmpl1 = self.mod_env.get_template("a/test.html")
264        assert tmpl1.render() == "BAR"
265        tmpl2 = self.mod_env.get_template("DICT_SOURCE")
266        assert tmpl2.render() == "DICT_TEMPLATE"
267
268    def test_prefix_loader(self, prefix_loader):
269        self.compile_down(prefix_loader)
270        self.mod_env.loader = loaders.PrefixLoader(
271            {
272                "MOD": self.mod_env.loader,
273                "DICT": loaders.DictLoader({"test.html": "DICT_TEMPLATE"}),
274            }
275        )
276        tmpl1 = self.mod_env.get_template("MOD/a/test.html")
277        assert tmpl1.render() == "BAR"
278        tmpl2 = self.mod_env.get_template("DICT/test.html")
279        assert tmpl2.render() == "DICT_TEMPLATE"
280
281    def test_path_as_pathlib(self, prefix_loader):
282        self.compile_down(prefix_loader)
283
284        mod_path = self.mod_env.loader.module.__path__[0]
285
286        import pathlib
287
288        mod_loader = loaders.ModuleLoader(pathlib.Path(mod_path))
289        self.mod_env = Environment(loader=mod_loader)
290
291        self._test_common()
292
293    def test_supports_pathlib_in_list_of_paths(self, prefix_loader):
294        self.compile_down(prefix_loader)
295
296        mod_path = self.mod_env.loader.module.__path__[0]
297
298        import pathlib
299
300        mod_loader = loaders.ModuleLoader([pathlib.Path(mod_path), "/tmp/templates"])
301        self.mod_env = Environment(loader=mod_loader)
302
303        self._test_common()
304
305
306@pytest.fixture()
307def package_dir_loader(monkeypatch):
308    monkeypatch.syspath_prepend(os.path.dirname(__file__))
309    return PackageLoader("res")
310
311
312@pytest.mark.parametrize(
313    ("template", "expect"), [("foo/test.html", "FOO"), ("test.html", "BAR")]
314)
315def test_package_dir_source(package_dir_loader, template, expect):
316    source, name, up_to_date = package_dir_loader.get_source(None, template)
317    assert source.rstrip() == expect
318    assert name.endswith(os.path.join(*split_template_path(template)))
319    assert up_to_date()
320
321
322def test_package_dir_list(package_dir_loader):
323    templates = package_dir_loader.list_templates()
324    assert "foo/test.html" in templates
325    assert "test.html" in templates
326
327
328@pytest.fixture()
329def package_zip_loader(monkeypatch):
330    monkeypatch.syspath_prepend(
331        os.path.join(os.path.dirname(__file__), "res", "package.zip")
332    )
333    return PackageLoader("t_pack")
334
335
336@pytest.mark.parametrize(
337    ("template", "expect"), [("foo/test.html", "FOO"), ("test.html", "BAR")]
338)
339def test_package_zip_source(package_zip_loader, template, expect):
340    source, name, up_to_date = package_zip_loader.get_source(None, template)
341    assert source.rstrip() == expect
342    assert name.endswith(os.path.join(*split_template_path(template)))
343    assert up_to_date is None
344
345
346@pytest.mark.xfail(
347    platform.python_implementation() == "PyPy",
348    reason="PyPy's zipimporter doesn't have a '_files' attribute.",
349    raises=TypeError,
350)
351def test_package_zip_list(package_zip_loader):
352    assert package_zip_loader.list_templates() == ["foo/test.html", "test.html"]
353
354
355def test_pep_451_import_hook():
356    class ImportHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
357        def find_spec(self, name, path=None, target=None):
358            if name != "res":
359                return None
360
361            spec = importlib.machinery.PathFinder.find_spec(name)
362            return importlib.util.spec_from_file_location(
363                name,
364                spec.origin,
365                loader=self,
366                submodule_search_locations=spec.submodule_search_locations,
367            )
368
369        def create_module(self, spec):
370            return None  # default behaviour is fine
371
372        def exec_module(self, module):
373            return None  # we need this to satisfy the interface, it's wrong
374
375    # ensure we restore `sys.meta_path` after putting in our loader
376    before = sys.meta_path[:]
377
378    try:
379        sys.meta_path.insert(0, ImportHook())
380        package_loader = PackageLoader("res")
381        assert "test.html" in package_loader.list_templates()
382    finally:
383        sys.meta_path[:] = before
384