Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jinja2/loaders.py: 23%
255 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:15 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:15 +0000
1"""API and implementations for loading templates from different data
2sources.
3"""
4import importlib.util
5import os
6import posixpath
7import sys
8import typing as t
9import weakref
10import zipimport
11from collections import abc
12from hashlib import sha1
13from importlib import import_module
14from types import ModuleType
16from .exceptions import TemplateNotFound
17from .utils import internalcode
19if t.TYPE_CHECKING:
20 from .environment import Environment
21 from .environment import Template
24def split_template_path(template: str) -> t.List[str]:
25 """Split a path into segments and perform a sanity check. If it detects
26 '..' in the path it will raise a `TemplateNotFound` error.
27 """
28 pieces = []
29 for piece in template.split("/"):
30 if (
31 os.sep in piece
32 or (os.path.altsep and os.path.altsep in piece)
33 or piece == os.path.pardir
34 ):
35 raise TemplateNotFound(template)
36 elif piece and piece != ".":
37 pieces.append(piece)
38 return pieces
41class BaseLoader:
42 """Baseclass for all loaders. Subclass this and override `get_source` to
43 implement a custom loading mechanism. The environment provides a
44 `get_template` method that calls the loader's `load` method to get the
45 :class:`Template` object.
47 A very basic example for a loader that looks up templates on the file
48 system could look like this::
50 from jinja2 import BaseLoader, TemplateNotFound
51 from os.path import join, exists, getmtime
53 class MyLoader(BaseLoader):
55 def __init__(self, path):
56 self.path = path
58 def get_source(self, environment, template):
59 path = join(self.path, template)
60 if not exists(path):
61 raise TemplateNotFound(template)
62 mtime = getmtime(path)
63 with open(path) as f:
64 source = f.read()
65 return source, path, lambda: mtime == getmtime(path)
66 """
68 #: if set to `False` it indicates that the loader cannot provide access
69 #: to the source of templates.
70 #:
71 #: .. versionadded:: 2.4
72 has_source_access = True
74 def get_source(
75 self, environment: "Environment", template: str
76 ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
77 """Get the template source, filename and reload helper for a template.
78 It's passed the environment and template name and has to return a
79 tuple in the form ``(source, filename, uptodate)`` or raise a
80 `TemplateNotFound` error if it can't locate the template.
82 The source part of the returned tuple must be the source of the
83 template as a string. The filename should be the name of the
84 file on the filesystem if it was loaded from there, otherwise
85 ``None``. The filename is used by Python for the tracebacks
86 if no loader extension is used.
88 The last item in the tuple is the `uptodate` function. If auto
89 reloading is enabled it's always called to check if the template
90 changed. No arguments are passed so the function must store the
91 old state somewhere (for example in a closure). If it returns `False`
92 the template will be reloaded.
93 """
94 if not self.has_source_access:
95 raise RuntimeError(
96 f"{type(self).__name__} cannot provide access to the source"
97 )
98 raise TemplateNotFound(template)
100 def list_templates(self) -> t.List[str]:
101 """Iterates over all templates. If the loader does not support that
102 it should raise a :exc:`TypeError` which is the default behavior.
103 """
104 raise TypeError("this loader cannot iterate over all templates")
106 @internalcode
107 def load(
108 self,
109 environment: "Environment",
110 name: str,
111 globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
112 ) -> "Template":
113 """Loads a template. This method looks up the template in the cache
114 or loads one by calling :meth:`get_source`. Subclasses should not
115 override this method as loaders working on collections of other
116 loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`)
117 will not call this method but `get_source` directly.
118 """
119 code = None
120 if globals is None:
121 globals = {}
123 # first we try to get the source for this template together
124 # with the filename and the uptodate function.
125 source, filename, uptodate = self.get_source(environment, name)
127 # try to load the code from the bytecode cache if there is a
128 # bytecode cache configured.
129 bcc = environment.bytecode_cache
130 if bcc is not None:
131 bucket = bcc.get_bucket(environment, name, filename, source)
132 code = bucket.code
134 # if we don't have code so far (not cached, no longer up to
135 # date) etc. we compile the template
136 if code is None:
137 code = environment.compile(source, name, filename)
139 # if the bytecode cache is available and the bucket doesn't
140 # have a code so far, we give the bucket the new code and put
141 # it back to the bytecode cache.
142 if bcc is not None and bucket.code is None:
143 bucket.code = code
144 bcc.set_bucket(bucket)
146 return environment.template_class.from_code(
147 environment, code, globals, uptodate
148 )
151class FileSystemLoader(BaseLoader):
152 """Load templates from a directory in the file system.
154 The path can be relative or absolute. Relative paths are relative to
155 the current working directory.
157 .. code-block:: python
159 loader = FileSystemLoader("templates")
161 A list of paths can be given. The directories will be searched in
162 order, stopping at the first matching template.
164 .. code-block:: python
166 loader = FileSystemLoader(["/override/templates", "/default/templates"])
168 :param searchpath: A path, or list of paths, to the directory that
169 contains the templates.
170 :param encoding: Use this encoding to read the text from template
171 files.
172 :param followlinks: Follow symbolic links in the path.
174 .. versionchanged:: 2.8
175 Added the ``followlinks`` parameter.
176 """
178 def __init__(
179 self,
180 searchpath: t.Union[
181 str, "os.PathLike[str]", t.Sequence[t.Union[str, "os.PathLike[str]"]]
182 ],
183 encoding: str = "utf-8",
184 followlinks: bool = False,
185 ) -> None:
186 if not isinstance(searchpath, abc.Iterable) or isinstance(searchpath, str):
187 searchpath = [searchpath]
189 self.searchpath = [os.fspath(p) for p in searchpath]
190 self.encoding = encoding
191 self.followlinks = followlinks
193 def get_source(
194 self, environment: "Environment", template: str
195 ) -> t.Tuple[str, str, t.Callable[[], bool]]:
196 pieces = split_template_path(template)
198 for searchpath in self.searchpath:
199 # Use posixpath even on Windows to avoid "drive:" or UNC
200 # segments breaking out of the search directory.
201 filename = posixpath.join(searchpath, *pieces)
203 if os.path.isfile(filename):
204 break
205 else:
206 raise TemplateNotFound(template)
208 with open(filename, encoding=self.encoding) as f:
209 contents = f.read()
211 mtime = os.path.getmtime(filename)
213 def uptodate() -> bool:
214 try:
215 return os.path.getmtime(filename) == mtime
216 except OSError:
217 return False
219 # Use normpath to convert Windows altsep to sep.
220 return contents, os.path.normpath(filename), uptodate
222 def list_templates(self) -> t.List[str]:
223 found = set()
224 for searchpath in self.searchpath:
225 walk_dir = os.walk(searchpath, followlinks=self.followlinks)
226 for dirpath, _, filenames in walk_dir:
227 for filename in filenames:
228 template = (
229 os.path.join(dirpath, filename)[len(searchpath) :]
230 .strip(os.sep)
231 .replace(os.sep, "/")
232 )
233 if template[:2] == "./":
234 template = template[2:]
235 if template not in found:
236 found.add(template)
237 return sorted(found)
240class PackageLoader(BaseLoader):
241 """Load templates from a directory in a Python package.
243 :param package_name: Import name of the package that contains the
244 template directory.
245 :param package_path: Directory within the imported package that
246 contains the templates.
247 :param encoding: Encoding of template files.
249 The following example looks up templates in the ``pages`` directory
250 within the ``project.ui`` package.
252 .. code-block:: python
254 loader = PackageLoader("project.ui", "pages")
256 Only packages installed as directories (standard pip behavior) or
257 zip/egg files (less common) are supported. The Python API for
258 introspecting data in packages is too limited to support other
259 installation methods the way this loader requires.
261 There is limited support for :pep:`420` namespace packages. The
262 template directory is assumed to only be in one namespace
263 contributor. Zip files contributing to a namespace are not
264 supported.
266 .. versionchanged:: 3.0
267 No longer uses ``setuptools`` as a dependency.
269 .. versionchanged:: 3.0
270 Limited PEP 420 namespace package support.
271 """
273 def __init__(
274 self,
275 package_name: str,
276 package_path: "str" = "templates",
277 encoding: str = "utf-8",
278 ) -> None:
279 package_path = os.path.normpath(package_path).rstrip(os.sep)
281 # normpath preserves ".", which isn't valid in zip paths.
282 if package_path == os.path.curdir:
283 package_path = ""
284 elif package_path[:2] == os.path.curdir + os.sep:
285 package_path = package_path[2:]
287 self.package_path = package_path
288 self.package_name = package_name
289 self.encoding = encoding
291 # Make sure the package exists. This also makes namespace
292 # packages work, otherwise get_loader returns None.
293 import_module(package_name)
294 spec = importlib.util.find_spec(package_name)
295 assert spec is not None, "An import spec was not found for the package."
296 loader = spec.loader
297 assert loader is not None, "A loader was not found for the package."
298 self._loader = loader
299 self._archive = None
300 template_root = None
302 if isinstance(loader, zipimport.zipimporter):
303 self._archive = loader.archive
304 pkgdir = next(iter(spec.submodule_search_locations)) # type: ignore
305 template_root = os.path.join(pkgdir, package_path).rstrip(os.sep)
306 else:
307 roots: t.List[str] = []
309 # One element for regular packages, multiple for namespace
310 # packages, or None for single module file.
311 if spec.submodule_search_locations:
312 roots.extend(spec.submodule_search_locations)
313 # A single module file, use the parent directory instead.
314 elif spec.origin is not None:
315 roots.append(os.path.dirname(spec.origin))
317 for root in roots:
318 root = os.path.join(root, package_path)
320 if os.path.isdir(root):
321 template_root = root
322 break
324 if template_root is None:
325 raise ValueError(
326 f"The {package_name!r} package was not installed in a"
327 " way that PackageLoader understands."
328 )
330 self._template_root = template_root
332 def get_source(
333 self, environment: "Environment", template: str
334 ) -> t.Tuple[str, str, t.Optional[t.Callable[[], bool]]]:
335 # Use posixpath even on Windows to avoid "drive:" or UNC
336 # segments breaking out of the search directory. Use normpath to
337 # convert Windows altsep to sep.
338 p = os.path.normpath(
339 posixpath.join(self._template_root, *split_template_path(template))
340 )
341 up_to_date: t.Optional[t.Callable[[], bool]]
343 if self._archive is None:
344 # Package is a directory.
345 if not os.path.isfile(p):
346 raise TemplateNotFound(template)
348 with open(p, "rb") as f:
349 source = f.read()
351 mtime = os.path.getmtime(p)
353 def up_to_date() -> bool:
354 return os.path.isfile(p) and os.path.getmtime(p) == mtime
356 else:
357 # Package is a zip file.
358 try:
359 source = self._loader.get_data(p) # type: ignore
360 except OSError as e:
361 raise TemplateNotFound(template) from e
363 # Could use the zip's mtime for all template mtimes, but
364 # would need to safely reload the module if it's out of
365 # date, so just report it as always current.
366 up_to_date = None
368 return source.decode(self.encoding), p, up_to_date
370 def list_templates(self) -> t.List[str]:
371 results: t.List[str] = []
373 if self._archive is None:
374 # Package is a directory.
375 offset = len(self._template_root)
377 for dirpath, _, filenames in os.walk(self._template_root):
378 dirpath = dirpath[offset:].lstrip(os.sep)
379 results.extend(
380 os.path.join(dirpath, name).replace(os.sep, "/")
381 for name in filenames
382 )
383 else:
384 if not hasattr(self._loader, "_files"):
385 raise TypeError(
386 "This zip import does not have the required"
387 " metadata to list templates."
388 )
390 # Package is a zip file.
391 prefix = self._template_root[len(self._archive) :].lstrip(os.sep) + os.sep
392 offset = len(prefix)
394 for name in self._loader._files.keys():
395 # Find names under the templates directory that aren't directories.
396 if name.startswith(prefix) and name[-1] != os.sep:
397 results.append(name[offset:].replace(os.sep, "/"))
399 results.sort()
400 return results
403class DictLoader(BaseLoader):
404 """Loads a template from a Python dict mapping template names to
405 template source. This loader is useful for unittesting:
407 >>> loader = DictLoader({'index.html': 'source here'})
409 Because auto reloading is rarely useful this is disabled per default.
410 """
412 def __init__(self, mapping: t.Mapping[str, str]) -> None:
413 self.mapping = mapping
415 def get_source(
416 self, environment: "Environment", template: str
417 ) -> t.Tuple[str, None, t.Callable[[], bool]]:
418 if template in self.mapping:
419 source = self.mapping[template]
420 return source, None, lambda: source == self.mapping.get(template)
421 raise TemplateNotFound(template)
423 def list_templates(self) -> t.List[str]:
424 return sorted(self.mapping)
427class FunctionLoader(BaseLoader):
428 """A loader that is passed a function which does the loading. The
429 function receives the name of the template and has to return either
430 a string with the template source, a tuple in the form ``(source,
431 filename, uptodatefunc)`` or `None` if the template does not exist.
433 >>> def load_template(name):
434 ... if name == 'index.html':
435 ... return '...'
436 ...
437 >>> loader = FunctionLoader(load_template)
439 The `uptodatefunc` is a function that is called if autoreload is enabled
440 and has to return `True` if the template is still up to date. For more
441 details have a look at :meth:`BaseLoader.get_source` which has the same
442 return value.
443 """
445 def __init__(
446 self,
447 load_func: t.Callable[
448 [str],
449 t.Optional[
450 t.Union[
451 str, t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]
452 ]
453 ],
454 ],
455 ) -> None:
456 self.load_func = load_func
458 def get_source(
459 self, environment: "Environment", template: str
460 ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
461 rv = self.load_func(template)
463 if rv is None:
464 raise TemplateNotFound(template)
466 if isinstance(rv, str):
467 return rv, None, None
469 return rv
472class PrefixLoader(BaseLoader):
473 """A loader that is passed a dict of loaders where each loader is bound
474 to a prefix. The prefix is delimited from the template by a slash per
475 default, which can be changed by setting the `delimiter` argument to
476 something else::
478 loader = PrefixLoader({
479 'app1': PackageLoader('mypackage.app1'),
480 'app2': PackageLoader('mypackage.app2')
481 })
483 By loading ``'app1/index.html'`` the file from the app1 package is loaded,
484 by loading ``'app2/index.html'`` the file from the second.
485 """
487 def __init__(
488 self, mapping: t.Mapping[str, BaseLoader], delimiter: str = "/"
489 ) -> None:
490 self.mapping = mapping
491 self.delimiter = delimiter
493 def get_loader(self, template: str) -> t.Tuple[BaseLoader, str]:
494 try:
495 prefix, name = template.split(self.delimiter, 1)
496 loader = self.mapping[prefix]
497 except (ValueError, KeyError) as e:
498 raise TemplateNotFound(template) from e
499 return loader, name
501 def get_source(
502 self, environment: "Environment", template: str
503 ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
504 loader, name = self.get_loader(template)
505 try:
506 return loader.get_source(environment, name)
507 except TemplateNotFound as e:
508 # re-raise the exception with the correct filename here.
509 # (the one that includes the prefix)
510 raise TemplateNotFound(template) from e
512 @internalcode
513 def load(
514 self,
515 environment: "Environment",
516 name: str,
517 globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
518 ) -> "Template":
519 loader, local_name = self.get_loader(name)
520 try:
521 return loader.load(environment, local_name, globals)
522 except TemplateNotFound as e:
523 # re-raise the exception with the correct filename here.
524 # (the one that includes the prefix)
525 raise TemplateNotFound(name) from e
527 def list_templates(self) -> t.List[str]:
528 result = []
529 for prefix, loader in self.mapping.items():
530 for template in loader.list_templates():
531 result.append(prefix + self.delimiter + template)
532 return result
535class ChoiceLoader(BaseLoader):
536 """This loader works like the `PrefixLoader` just that no prefix is
537 specified. If a template could not be found by one loader the next one
538 is tried.
540 >>> loader = ChoiceLoader([
541 ... FileSystemLoader('/path/to/user/templates'),
542 ... FileSystemLoader('/path/to/system/templates')
543 ... ])
545 This is useful if you want to allow users to override builtin templates
546 from a different location.
547 """
549 def __init__(self, loaders: t.Sequence[BaseLoader]) -> None:
550 self.loaders = loaders
552 def get_source(
553 self, environment: "Environment", template: str
554 ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
555 for loader in self.loaders:
556 try:
557 return loader.get_source(environment, template)
558 except TemplateNotFound:
559 pass
560 raise TemplateNotFound(template)
562 @internalcode
563 def load(
564 self,
565 environment: "Environment",
566 name: str,
567 globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
568 ) -> "Template":
569 for loader in self.loaders:
570 try:
571 return loader.load(environment, name, globals)
572 except TemplateNotFound:
573 pass
574 raise TemplateNotFound(name)
576 def list_templates(self) -> t.List[str]:
577 found = set()
578 for loader in self.loaders:
579 found.update(loader.list_templates())
580 return sorted(found)
583class _TemplateModule(ModuleType):
584 """Like a normal module but with support for weak references"""
587class ModuleLoader(BaseLoader):
588 """This loader loads templates from precompiled templates.
590 Example usage:
592 >>> loader = ChoiceLoader([
593 ... ModuleLoader('/path/to/compiled/templates'),
594 ... FileSystemLoader('/path/to/templates')
595 ... ])
597 Templates can be precompiled with :meth:`Environment.compile_templates`.
598 """
600 has_source_access = False
602 def __init__(
603 self,
604 path: t.Union[
605 str, "os.PathLike[str]", t.Sequence[t.Union[str, "os.PathLike[str]"]]
606 ],
607 ) -> None:
608 package_name = f"_jinja2_module_templates_{id(self):x}"
610 # create a fake module that looks for the templates in the
611 # path given.
612 mod = _TemplateModule(package_name)
614 if not isinstance(path, abc.Iterable) or isinstance(path, str):
615 path = [path]
617 mod.__path__ = [os.fspath(p) for p in path]
619 sys.modules[package_name] = weakref.proxy(
620 mod, lambda x: sys.modules.pop(package_name, None)
621 )
623 # the only strong reference, the sys.modules entry is weak
624 # so that the garbage collector can remove it once the
625 # loader that created it goes out of business.
626 self.module = mod
627 self.package_name = package_name
629 @staticmethod
630 def get_template_key(name: str) -> str:
631 return "tmpl_" + sha1(name.encode("utf-8")).hexdigest()
633 @staticmethod
634 def get_module_filename(name: str) -> str:
635 return ModuleLoader.get_template_key(name) + ".py"
637 @internalcode
638 def load(
639 self,
640 environment: "Environment",
641 name: str,
642 globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
643 ) -> "Template":
644 key = self.get_template_key(name)
645 module = f"{self.package_name}.{key}"
646 mod = getattr(self.module, module, None)
648 if mod is None:
649 try:
650 mod = __import__(module, None, None, ["root"])
651 except ImportError as e:
652 raise TemplateNotFound(name) from e
654 # remove the entry from sys.modules, we only want the attribute
655 # on the module object we have stored on the loader.
656 sys.modules.pop(module, None)
658 if globals is None:
659 globals = {}
661 return environment.template_class.from_module_dict(
662 environment, mod.__dict__, globals
663 )