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