Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jinja2/loaders.py: 23%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""API and implementations for loading templates from different data
2sources.
3"""
5import importlib.util
6import os
7import posixpath
8import sys
9import typing as t
10import weakref
11import zipimport
12from collections import abc
13from hashlib import sha1
14from importlib import import_module
15from types import ModuleType
17from .exceptions import TemplateNotFound
18from .utils import internalcode
20if t.TYPE_CHECKING:
21 from .environment import Environment
22 from .environment import Template
25def split_template_path(template: str) -> t.List[str]:
26 """Split a path into segments and perform a sanity check. If it detects
27 '..' in the path it will raise a `TemplateNotFound` error.
28 """
29 pieces = []
30 for piece in template.split("/"):
31 if (
32 os.sep in piece
33 or (os.path.altsep and os.path.altsep in piece)
34 or piece == os.path.pardir
35 ):
36 raise TemplateNotFound(template)
37 elif piece and piece != ".":
38 pieces.append(piece)
39 return pieces
42class BaseLoader:
43 """Baseclass for all loaders. Subclass this and override `get_source` to
44 implement a custom loading mechanism. The environment provides a
45 `get_template` method that calls the loader's `load` method to get the
46 :class:`Template` object.
48 A very basic example for a loader that looks up templates on the file
49 system could look like this::
51 from jinja2 import BaseLoader, TemplateNotFound
52 from os.path import join, exists, getmtime
54 class MyLoader(BaseLoader):
56 def __init__(self, path):
57 self.path = path
59 def get_source(self, environment, template):
60 path = join(self.path, template)
61 if not exists(path):
62 raise TemplateNotFound(template)
63 mtime = getmtime(path)
64 with open(path) as f:
65 source = f.read()
66 return source, path, lambda: mtime == getmtime(path)
67 """
69 #: if set to `False` it indicates that the loader cannot provide access
70 #: to the source of templates.
71 #:
72 #: .. versionadded:: 2.4
73 has_source_access = True
75 def get_source(
76 self, environment: "Environment", template: str
77 ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
78 """Get the template source, filename and reload helper for a template.
79 It's passed the environment and template name and has to return a
80 tuple in the form ``(source, filename, uptodate)`` or raise a
81 `TemplateNotFound` error if it can't locate the template.
83 The source part of the returned tuple must be the source of the
84 template as a string. The filename should be the name of the
85 file on the filesystem if it was loaded from there, otherwise
86 ``None``. The filename is used by Python for the tracebacks
87 if no loader extension is used.
89 The last item in the tuple is the `uptodate` function. If auto
90 reloading is enabled it's always called to check if the template
91 changed. No arguments are passed so the function must store the
92 old state somewhere (for example in a closure). If it returns `False`
93 the template will be reloaded.
94 """
95 if not self.has_source_access:
96 raise RuntimeError(
97 f"{type(self).__name__} cannot provide access to the source"
98 )
99 raise TemplateNotFound(template)
101 def list_templates(self) -> t.List[str]:
102 """Iterates over all templates. If the loader does not support that
103 it should raise a :exc:`TypeError` which is the default behavior.
104 """
105 raise TypeError("this loader cannot iterate over all templates")
107 @internalcode
108 def load(
109 self,
110 environment: "Environment",
111 name: str,
112 globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
113 ) -> "Template":
114 """Loads a template. This method looks up the template in the cache
115 or loads one by calling :meth:`get_source`. Subclasses should not
116 override this method as loaders working on collections of other
117 loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`)
118 will not call this method but `get_source` directly.
119 """
120 code = None
121 if globals is None:
122 globals = {}
124 # first we try to get the source for this template together
125 # with the filename and the uptodate function.
126 source, filename, uptodate = self.get_source(environment, name)
128 # try to load the code from the bytecode cache if there is a
129 # bytecode cache configured.
130 bcc = environment.bytecode_cache
131 if bcc is not None:
132 bucket = bcc.get_bucket(environment, name, filename, source)
133 code = bucket.code
135 # if we don't have code so far (not cached, no longer up to
136 # date) etc. we compile the template
137 if code is None:
138 code = environment.compile(source, name, filename)
140 # if the bytecode cache is available and the bucket doesn't
141 # have a code so far, we give the bucket the new code and put
142 # it back to the bytecode cache.
143 if bcc is not None and bucket.code is None:
144 bucket.code = code
145 bcc.set_bucket(bucket)
147 return environment.template_class.from_code(
148 environment, code, globals, uptodate
149 )
152class FileSystemLoader(BaseLoader):
153 """Load templates from a directory in the file system.
155 The path can be relative or absolute. Relative paths are relative to
156 the current working directory.
158 .. code-block:: python
160 loader = FileSystemLoader("templates")
162 A list of paths can be given. The directories will be searched in
163 order, stopping at the first matching template.
165 .. code-block:: python
167 loader = FileSystemLoader(["/override/templates", "/default/templates"])
169 :param searchpath: A path, or list of paths, to the directory that
170 contains the templates.
171 :param encoding: Use this encoding to read the text from template
172 files.
173 :param followlinks: Follow symbolic links in the path.
175 .. versionchanged:: 2.8
176 Added the ``followlinks`` parameter.
177 """
179 def __init__(
180 self,
181 searchpath: t.Union[
182 str, "os.PathLike[str]", t.Sequence[t.Union[str, "os.PathLike[str]"]]
183 ],
184 encoding: str = "utf-8",
185 followlinks: bool = False,
186 ) -> None:
187 if not isinstance(searchpath, abc.Iterable) or isinstance(searchpath, str):
188 searchpath = [searchpath]
190 self.searchpath = [os.fspath(p) for p in searchpath]
191 self.encoding = encoding
192 self.followlinks = followlinks
194 def get_source(
195 self, environment: "Environment", template: str
196 ) -> t.Tuple[str, str, t.Callable[[], bool]]:
197 pieces = split_template_path(template)
199 for searchpath in self.searchpath:
200 # Use posixpath even on Windows to avoid "drive:" or UNC
201 # segments breaking out of the search directory.
202 filename = posixpath.join(searchpath, *pieces)
204 if os.path.isfile(filename):
205 break
206 else:
207 raise TemplateNotFound(template)
209 with open(filename, encoding=self.encoding) as f:
210 contents = f.read()
212 mtime = os.path.getmtime(filename)
214 def uptodate() -> bool:
215 try:
216 return os.path.getmtime(filename) == mtime
217 except OSError:
218 return False
220 # Use normpath to convert Windows altsep to sep.
221 return contents, os.path.normpath(filename), uptodate
223 def list_templates(self) -> t.List[str]:
224 found = set()
225 for searchpath in self.searchpath:
226 walk_dir = os.walk(searchpath, followlinks=self.followlinks)
227 for dirpath, _, filenames in walk_dir:
228 for filename in filenames:
229 template = (
230 os.path.join(dirpath, filename)[len(searchpath) :]
231 .strip(os.sep)
232 .replace(os.sep, "/")
233 )
234 if template[:2] == "./":
235 template = template[2:]
236 if template not in found:
237 found.add(template)
238 return sorted(found)
241class PackageLoader(BaseLoader):
242 """Load templates from a directory in a Python package.
244 :param package_name: Import name of the package that contains the
245 template directory.
246 :param package_path: Directory within the imported package that
247 contains the templates.
248 :param encoding: Encoding of template files.
250 The following example looks up templates in the ``pages`` directory
251 within the ``project.ui`` package.
253 .. code-block:: python
255 loader = PackageLoader("project.ui", "pages")
257 Only packages installed as directories (standard pip behavior) or
258 zip/egg files (less common) are supported. The Python API for
259 introspecting data in packages is too limited to support other
260 installation methods the way this loader requires.
262 There is limited support for :pep:`420` namespace packages. The
263 template directory is assumed to only be in one namespace
264 contributor. Zip files contributing to a namespace are not
265 supported.
267 .. versionchanged:: 3.0
268 No longer uses ``setuptools`` as a dependency.
270 .. versionchanged:: 3.0
271 Limited PEP 420 namespace package support.
272 """
274 def __init__(
275 self,
276 package_name: str,
277 package_path: "str" = "templates",
278 encoding: str = "utf-8",
279 ) -> None:
280 package_path = os.path.normpath(package_path).rstrip(os.sep)
282 # normpath preserves ".", which isn't valid in zip paths.
283 if package_path == os.path.curdir:
284 package_path = ""
285 elif package_path[:2] == os.path.curdir + os.sep:
286 package_path = package_path[2:]
288 self.package_path = package_path
289 self.package_name = package_name
290 self.encoding = encoding
292 # Make sure the package exists. This also makes namespace
293 # packages work, otherwise get_loader returns None.
294 import_module(package_name)
295 spec = importlib.util.find_spec(package_name)
296 assert spec is not None, "An import spec was not found for the package."
297 loader = spec.loader
298 assert loader is not None, "A loader was not found for the package."
299 self._loader = loader
300 self._archive = None
301 template_root = None
303 if isinstance(loader, zipimport.zipimporter):
304 self._archive = loader.archive
305 pkgdir = next(iter(spec.submodule_search_locations)) # type: ignore
306 template_root = os.path.join(pkgdir, package_path).rstrip(os.sep)
307 else:
308 roots: t.List[str] = []
310 # One element for regular packages, multiple for namespace
311 # packages, or None for single module file.
312 if spec.submodule_search_locations:
313 roots.extend(spec.submodule_search_locations)
314 # A single module file, use the parent directory instead.
315 elif spec.origin is not None:
316 roots.append(os.path.dirname(spec.origin))
318 for root in roots:
319 root = os.path.join(root, package_path)
321 if os.path.isdir(root):
322 template_root = root
323 break
325 if template_root is None:
326 raise ValueError(
327 f"The {package_name!r} package was not installed in a"
328 " way that PackageLoader understands."
329 )
331 self._template_root = template_root
333 def get_source(
334 self, environment: "Environment", template: str
335 ) -> t.Tuple[str, str, t.Optional[t.Callable[[], bool]]]:
336 # Use posixpath even on Windows to avoid "drive:" or UNC
337 # segments breaking out of the search directory. Use normpath to
338 # convert Windows altsep to sep.
339 p = os.path.normpath(
340 posixpath.join(self._template_root, *split_template_path(template))
341 )
342 up_to_date: t.Optional[t.Callable[[], bool]]
344 if self._archive is None:
345 # Package is a directory.
346 if not os.path.isfile(p):
347 raise TemplateNotFound(template)
349 with open(p, "rb") as f:
350 source = f.read()
352 mtime = os.path.getmtime(p)
354 def up_to_date() -> bool:
355 return os.path.isfile(p) and os.path.getmtime(p) == mtime
357 else:
358 # Package is a zip file.
359 try:
360 source = self._loader.get_data(p) # type: ignore
361 except OSError as e:
362 raise TemplateNotFound(template) from e
364 # Could use the zip's mtime for all template mtimes, but
365 # would need to safely reload the module if it's out of
366 # date, so just report it as always current.
367 up_to_date = None
369 return source.decode(self.encoding), p, up_to_date
371 def list_templates(self) -> t.List[str]:
372 results: t.List[str] = []
374 if self._archive is None:
375 # Package is a directory.
376 offset = len(self._template_root)
378 for dirpath, _, filenames in os.walk(self._template_root):
379 dirpath = dirpath[offset:].lstrip(os.sep)
380 results.extend(
381 os.path.join(dirpath, name).replace(os.sep, "/")
382 for name in filenames
383 )
384 else:
385 if not hasattr(self._loader, "_files"):
386 raise TypeError(
387 "This zip import does not have the required"
388 " metadata to list templates."
389 )
391 # Package is a zip file.
392 prefix = self._template_root[len(self._archive) :].lstrip(os.sep) + os.sep
393 offset = len(prefix)
395 for name in self._loader._files.keys():
396 # Find names under the templates directory that aren't directories.
397 if name.startswith(prefix) and name[-1] != os.sep:
398 results.append(name[offset:].replace(os.sep, "/"))
400 results.sort()
401 return results
404class DictLoader(BaseLoader):
405 """Loads a template from a Python dict mapping template names to
406 template source. This loader is useful for unittesting:
408 >>> loader = DictLoader({'index.html': 'source here'})
410 Because auto reloading is rarely useful this is disabled per default.
411 """
413 def __init__(self, mapping: t.Mapping[str, str]) -> None:
414 self.mapping = mapping
416 def get_source(
417 self, environment: "Environment", template: str
418 ) -> t.Tuple[str, None, t.Callable[[], bool]]:
419 if template in self.mapping:
420 source = self.mapping[template]
421 return source, None, lambda: source == self.mapping.get(template)
422 raise TemplateNotFound(template)
424 def list_templates(self) -> t.List[str]:
425 return sorted(self.mapping)
428class FunctionLoader(BaseLoader):
429 """A loader that is passed a function which does the loading. The
430 function receives the name of the template and has to return either
431 a string with the template source, a tuple in the form ``(source,
432 filename, uptodatefunc)`` or `None` if the template does not exist.
434 >>> def load_template(name):
435 ... if name == 'index.html':
436 ... return '...'
437 ...
438 >>> loader = FunctionLoader(load_template)
440 The `uptodatefunc` is a function that is called if autoreload is enabled
441 and has to return `True` if the template is still up to date. For more
442 details have a look at :meth:`BaseLoader.get_source` which has the same
443 return value.
444 """
446 def __init__(
447 self,
448 load_func: t.Callable[
449 [str],
450 t.Optional[
451 t.Union[
452 str, t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]
453 ]
454 ],
455 ],
456 ) -> None:
457 self.load_func = load_func
459 def get_source(
460 self, environment: "Environment", template: str
461 ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
462 rv = self.load_func(template)
464 if rv is None:
465 raise TemplateNotFound(template)
467 if isinstance(rv, str):
468 return rv, None, None
470 return rv
473class PrefixLoader(BaseLoader):
474 """A loader that is passed a dict of loaders where each loader is bound
475 to a prefix. The prefix is delimited from the template by a slash per
476 default, which can be changed by setting the `delimiter` argument to
477 something else::
479 loader = PrefixLoader({
480 'app1': PackageLoader('mypackage.app1'),
481 'app2': PackageLoader('mypackage.app2')
482 })
484 By loading ``'app1/index.html'`` the file from the app1 package is loaded,
485 by loading ``'app2/index.html'`` the file from the second.
486 """
488 def __init__(
489 self, mapping: t.Mapping[str, BaseLoader], delimiter: str = "/"
490 ) -> None:
491 self.mapping = mapping
492 self.delimiter = delimiter
494 def get_loader(self, template: str) -> t.Tuple[BaseLoader, str]:
495 try:
496 prefix, name = template.split(self.delimiter, 1)
497 loader = self.mapping[prefix]
498 except (ValueError, KeyError) as e:
499 raise TemplateNotFound(template) from e
500 return loader, name
502 def get_source(
503 self, environment: "Environment", template: str
504 ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
505 loader, name = self.get_loader(template)
506 try:
507 return loader.get_source(environment, name)
508 except TemplateNotFound as e:
509 # re-raise the exception with the correct filename here.
510 # (the one that includes the prefix)
511 raise TemplateNotFound(template) from e
513 @internalcode
514 def load(
515 self,
516 environment: "Environment",
517 name: str,
518 globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
519 ) -> "Template":
520 loader, local_name = self.get_loader(name)
521 try:
522 return loader.load(environment, local_name, globals)
523 except TemplateNotFound as e:
524 # re-raise the exception with the correct filename here.
525 # (the one that includes the prefix)
526 raise TemplateNotFound(name) from e
528 def list_templates(self) -> t.List[str]:
529 result = []
530 for prefix, loader in self.mapping.items():
531 for template in loader.list_templates():
532 result.append(prefix + self.delimiter + template)
533 return result
536class ChoiceLoader(BaseLoader):
537 """This loader works like the `PrefixLoader` just that no prefix is
538 specified. If a template could not be found by one loader the next one
539 is tried.
541 >>> loader = ChoiceLoader([
542 ... FileSystemLoader('/path/to/user/templates'),
543 ... FileSystemLoader('/path/to/system/templates')
544 ... ])
546 This is useful if you want to allow users to override builtin templates
547 from a different location.
548 """
550 def __init__(self, loaders: t.Sequence[BaseLoader]) -> None:
551 self.loaders = loaders
553 def get_source(
554 self, environment: "Environment", template: str
555 ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
556 for loader in self.loaders:
557 try:
558 return loader.get_source(environment, template)
559 except TemplateNotFound:
560 pass
561 raise TemplateNotFound(template)
563 @internalcode
564 def load(
565 self,
566 environment: "Environment",
567 name: str,
568 globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
569 ) -> "Template":
570 for loader in self.loaders:
571 try:
572 return loader.load(environment, name, globals)
573 except TemplateNotFound:
574 pass
575 raise TemplateNotFound(name)
577 def list_templates(self) -> t.List[str]:
578 found = set()
579 for loader in self.loaders:
580 found.update(loader.list_templates())
581 return sorted(found)
584class _TemplateModule(ModuleType):
585 """Like a normal module but with support for weak references"""
588class ModuleLoader(BaseLoader):
589 """This loader loads templates from precompiled templates.
591 Example usage:
593 >>> loader = ChoiceLoader([
594 ... ModuleLoader('/path/to/compiled/templates'),
595 ... FileSystemLoader('/path/to/templates')
596 ... ])
598 Templates can be precompiled with :meth:`Environment.compile_templates`.
599 """
601 has_source_access = False
603 def __init__(
604 self,
605 path: t.Union[
606 str, "os.PathLike[str]", t.Sequence[t.Union[str, "os.PathLike[str]"]]
607 ],
608 ) -> None:
609 package_name = f"_jinja2_module_templates_{id(self):x}"
611 # create a fake module that looks for the templates in the
612 # path given.
613 mod = _TemplateModule(package_name)
615 if not isinstance(path, abc.Iterable) or isinstance(path, str):
616 path = [path]
618 mod.__path__ = [os.fspath(p) for p in path]
620 sys.modules[package_name] = weakref.proxy(
621 mod, lambda x: sys.modules.pop(package_name, None)
622 )
624 # the only strong reference, the sys.modules entry is weak
625 # so that the garbage collector can remove it once the
626 # loader that created it goes out of business.
627 self.module = mod
628 self.package_name = package_name
630 @staticmethod
631 def get_template_key(name: str) -> str:
632 return "tmpl_" + sha1(name.encode("utf-8")).hexdigest()
634 @staticmethod
635 def get_module_filename(name: str) -> str:
636 return ModuleLoader.get_template_key(name) + ".py"
638 @internalcode
639 def load(
640 self,
641 environment: "Environment",
642 name: str,
643 globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
644 ) -> "Template":
645 key = self.get_template_key(name)
646 module = f"{self.package_name}.{key}"
647 mod = getattr(self.module, module, None)
649 if mod is None:
650 try:
651 mod = __import__(module, None, None, ["root"])
652 except ImportError as e:
653 raise TemplateNotFound(name) from e
655 # remove the entry from sys.modules, we only want the attribute
656 # on the module object we have stored on the loader.
657 sys.modules.pop(module, None)
659 if globals is None:
660 globals = {}
662 return environment.template_class.from_module_dict(
663 environment, mod.__dict__, globals
664 )