Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/jinja2/loaders.py: 26%
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) -> 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 ) -> tuple[str, str | None, t.Callable[[], bool] | None]:
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) -> 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.MutableMapping[str, t.Any] | None = 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 ) -> 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 plural = "path" if len(self.searchpath) == 1 else "paths"
208 paths_str = ", ".join(repr(p) for p in self.searchpath)
209 raise TemplateNotFound(
210 template,
211 f"{template!r} not found in search {plural}: {paths_str}",
212 )
214 with open(filename, encoding=self.encoding) as f:
215 contents = f.read()
217 mtime = os.path.getmtime(filename)
219 def uptodate() -> bool:
220 try:
221 return os.path.getmtime(filename) == mtime
222 except OSError:
223 return False
225 # Use normpath to convert Windows altsep to sep.
226 return contents, os.path.normpath(filename), uptodate
228 def list_templates(self) -> list[str]:
229 found = set()
230 for searchpath in self.searchpath:
231 walk_dir = os.walk(searchpath, followlinks=self.followlinks)
232 for dirpath, _, filenames in walk_dir:
233 for filename in filenames:
234 template = (
235 os.path.join(dirpath, filename)[len(searchpath) :]
236 .strip(os.sep)
237 .replace(os.sep, "/")
238 )
239 if template[:2] == "./":
240 template = template[2:]
241 if template not in found:
242 found.add(template)
243 return sorted(found)
246if sys.version_info >= (3, 13):
248 def _get_zipimporter_files(z: t.Any) -> dict[str, object]:
249 try:
250 get_files = z._get_files
251 except AttributeError as e:
252 raise TypeError(
253 "This zip import does not have the required metadata to list templates."
254 ) from e
255 return get_files()
257else:
259 def _get_zipimporter_files(z: t.Any) -> dict[str, object]:
260 try:
261 files = z._files
262 except AttributeError as e:
263 raise TypeError(
264 "This zip import does not have the required metadata to list templates."
265 ) from e
266 return files # type: ignore[no-any-return]
269class PackageLoader(BaseLoader):
270 """Load templates from a directory in a Python package.
272 :param package_name: Import name of the package that contains the
273 template directory.
274 :param package_path: Directory within the imported package that
275 contains the templates.
276 :param encoding: Encoding of template files.
278 The following example looks up templates in the ``pages`` directory
279 within the ``project.ui`` package.
281 .. code-block:: python
283 loader = PackageLoader("project.ui", "pages")
285 Only packages installed as directories (standard pip behavior) or
286 zip/egg files (less common) are supported. The Python API for
287 introspecting data in packages is too limited to support other
288 installation methods the way this loader requires.
290 There is limited support for :pep:`420` namespace packages. The
291 template directory is assumed to only be in one namespace
292 contributor. Zip files contributing to a namespace are not
293 supported.
295 .. versionchanged:: 3.0
296 No longer uses ``setuptools`` as a dependency.
298 .. versionchanged:: 3.0
299 Limited PEP 420 namespace package support.
300 """
302 def __init__(
303 self,
304 package_name: str,
305 package_path: "str" = "templates",
306 encoding: str = "utf-8",
307 ) -> None:
308 package_path = os.path.normpath(package_path).rstrip(os.sep)
310 # normpath preserves ".", which isn't valid in zip paths.
311 if package_path == os.path.curdir:
312 package_path = ""
313 elif package_path[:2] == os.path.curdir + os.sep:
314 package_path = package_path[2:]
316 self.package_path = package_path
317 self.package_name = package_name
318 self.encoding = encoding
320 # Make sure the package exists. This also makes namespace
321 # packages work, otherwise get_loader returns None.
322 import_module(package_name)
323 spec = importlib.util.find_spec(package_name)
324 assert spec is not None, "An import spec was not found for the package."
325 loader = spec.loader
326 assert loader is not None, "A loader was not found for the package."
327 self._loader = loader
328 self._archive = None
330 if isinstance(loader, zipimport.zipimporter):
331 self._archive = loader.archive
332 pkgdir = next(iter(spec.submodule_search_locations)) # type: ignore
333 template_root = os.path.join(pkgdir, package_path).rstrip(os.sep)
334 else:
335 roots: list[str] = []
337 # One element for regular packages, multiple for namespace
338 # packages, or None for single module file.
339 if spec.submodule_search_locations:
340 roots.extend(spec.submodule_search_locations)
341 # A single module file, use the parent directory instead.
342 elif spec.origin is not None:
343 roots.append(os.path.dirname(spec.origin))
345 if not roots:
346 raise ValueError(
347 f"The {package_name!r} package was not installed in a"
348 " way that PackageLoader understands."
349 )
351 for root in roots:
352 root = os.path.join(root, package_path)
354 if os.path.isdir(root):
355 template_root = root
356 break
357 else:
358 raise ValueError(
359 f"PackageLoader could not find a {package_path!r} directory"
360 f" in the {package_name!r} package."
361 )
363 self._template_root = template_root
365 def get_source(
366 self, environment: "Environment", template: str
367 ) -> tuple[str, str, t.Callable[[], bool] | None]:
368 # Use posixpath even on Windows to avoid "drive:" or UNC
369 # segments breaking out of the search directory. Use normpath to
370 # convert Windows altsep to sep.
371 p = os.path.normpath(
372 posixpath.join(self._template_root, *split_template_path(template))
373 )
374 up_to_date: t.Callable[[], bool] | None
376 if self._archive is None:
377 # Package is a directory.
378 if not os.path.isfile(p):
379 raise TemplateNotFound(template)
381 with open(p, "rb") as f:
382 source = f.read()
384 mtime = os.path.getmtime(p)
386 def up_to_date() -> bool:
387 return os.path.isfile(p) and os.path.getmtime(p) == mtime
389 else:
390 # Package is a zip file.
391 try:
392 source = self._loader.get_data(p) # type: ignore
393 except OSError as e:
394 raise TemplateNotFound(template) from e
396 # Could use the zip's mtime for all template mtimes, but
397 # would need to safely reload the module if it's out of
398 # date, so just report it as always current.
399 up_to_date = None
401 return source.decode(self.encoding), p, up_to_date
403 def list_templates(self) -> list[str]:
404 results: list[str] = []
406 if self._archive is None:
407 # Package is a directory.
408 offset = len(self._template_root)
410 for dirpath, _, filenames in os.walk(self._template_root):
411 dirpath = dirpath[offset:].lstrip(os.sep)
412 results.extend(
413 os.path.join(dirpath, name).replace(os.sep, "/")
414 for name in filenames
415 )
416 else:
417 files = _get_zipimporter_files(self._loader)
419 # Package is a zip file.
420 prefix = self._template_root[len(self._archive) :].lstrip(os.sep) + os.sep
421 offset = len(prefix)
423 for name in files:
424 # Find names under the templates directory that aren't directories.
425 if name.startswith(prefix) and name[-1] != os.sep:
426 results.append(name[offset:].replace(os.sep, "/"))
428 results.sort()
429 return results
432class DictLoader(BaseLoader):
433 """Loads a template from a Python dict mapping template names to
434 template source. This loader is useful for unittesting:
436 >>> loader = DictLoader({'index.html': 'source here'})
438 Because auto reloading is rarely useful this is disabled by default.
439 """
441 def __init__(self, mapping: t.Mapping[str, str]) -> None:
442 self.mapping = mapping
444 def get_source(
445 self, environment: "Environment", template: str
446 ) -> tuple[str, None, t.Callable[[], bool]]:
447 if template in self.mapping:
448 source = self.mapping[template]
449 return source, None, lambda: source == self.mapping.get(template)
450 raise TemplateNotFound(template)
452 def list_templates(self) -> list[str]:
453 return sorted(self.mapping)
456class FunctionLoader(BaseLoader):
457 """A loader that is passed a function which does the loading. The
458 function receives the name of the template and has to return either
459 a string with the template source, a tuple in the form ``(source,
460 filename, uptodatefunc)`` or `None` if the template does not exist.
462 >>> def load_template(name):
463 ... if name == 'index.html':
464 ... return '...'
465 ...
466 >>> loader = FunctionLoader(load_template)
468 The `uptodatefunc` is a function that is called if autoreload is enabled
469 and has to return `True` if the template is still up to date. For more
470 details have a look at :meth:`BaseLoader.get_source` which has the same
471 return value.
472 """
474 def __init__(
475 self,
476 load_func: t.Callable[
477 [str],
478 str | tuple[str, str | None, t.Callable[[], bool] | None] | None,
479 ],
480 ) -> None:
481 self.load_func = load_func
483 def get_source(
484 self, environment: "Environment", template: str
485 ) -> tuple[str, str | None, t.Callable[[], bool] | None]:
486 rv = self.load_func(template)
488 if rv is None:
489 raise TemplateNotFound(template)
491 if isinstance(rv, str):
492 return rv, None, None
494 return rv
497class PrefixLoader(BaseLoader):
498 """A loader that is passed a dict of loaders where each loader is bound
499 to a prefix. The prefix is delimited from the template by a slash per
500 default, which can be changed by setting the `delimiter` argument to
501 something else::
503 loader = PrefixLoader({
504 'app1': PackageLoader('mypackage.app1'),
505 'app2': PackageLoader('mypackage.app2')
506 })
508 By loading ``'app1/index.html'`` the file from the app1 package is loaded,
509 by loading ``'app2/index.html'`` the file from the second.
510 """
512 def __init__(
513 self, mapping: t.Mapping[str, BaseLoader], delimiter: str = "/"
514 ) -> None:
515 self.mapping = mapping
516 self.delimiter = delimiter
518 def get_loader(self, template: str) -> tuple[BaseLoader, str]:
519 try:
520 prefix, name = template.split(self.delimiter, 1)
521 loader = self.mapping[prefix]
522 except (ValueError, KeyError) as e:
523 raise TemplateNotFound(template) from e
524 return loader, name
526 def get_source(
527 self, environment: "Environment", template: str
528 ) -> tuple[str, str | None, t.Callable[[], bool] | None]:
529 loader, name = self.get_loader(template)
530 try:
531 return loader.get_source(environment, name)
532 except TemplateNotFound as e:
533 # re-raise the exception with the correct filename here.
534 # (the one that includes the prefix)
535 raise TemplateNotFound(template) from e
537 @internalcode
538 def load(
539 self,
540 environment: "Environment",
541 name: str,
542 globals: t.MutableMapping[str, t.Any] | None = None,
543 ) -> "Template":
544 loader, local_name = self.get_loader(name)
545 try:
546 return loader.load(environment, local_name, globals)
547 except TemplateNotFound as e:
548 # re-raise the exception with the correct filename here.
549 # (the one that includes the prefix)
550 raise TemplateNotFound(name) from e
552 def list_templates(self) -> list[str]:
553 result = []
554 for prefix, loader in self.mapping.items():
555 for template in loader.list_templates():
556 result.append(prefix + self.delimiter + template)
557 return result
560class ChoiceLoader(BaseLoader):
561 """This loader works like the `PrefixLoader` just that no prefix is
562 specified. If a template could not be found by one loader the next one
563 is tried.
565 >>> loader = ChoiceLoader([
566 ... FileSystemLoader('/path/to/user/templates'),
567 ... FileSystemLoader('/path/to/system/templates')
568 ... ])
570 This is useful if you want to allow users to override builtin templates
571 from a different location.
572 """
574 def __init__(self, loaders: t.Sequence[BaseLoader]) -> None:
575 self.loaders = loaders
577 def get_source(
578 self, environment: "Environment", template: str
579 ) -> tuple[str, str | None, t.Callable[[], bool] | None]:
580 for loader in self.loaders:
581 try:
582 return loader.get_source(environment, template)
583 except TemplateNotFound:
584 pass
585 raise TemplateNotFound(template)
587 @internalcode
588 def load(
589 self,
590 environment: "Environment",
591 name: str,
592 globals: t.MutableMapping[str, t.Any] | None = None,
593 ) -> "Template":
594 for loader in self.loaders:
595 try:
596 return loader.load(environment, name, globals)
597 except TemplateNotFound:
598 pass
599 raise TemplateNotFound(name)
601 def list_templates(self) -> list[str]:
602 found = set()
603 for loader in self.loaders:
604 found.update(loader.list_templates())
605 return sorted(found)
608class _TemplateModule(ModuleType):
609 """Like a normal module but with support for weak references"""
612class ModuleLoader(BaseLoader):
613 """This loader loads templates from precompiled templates.
615 Example usage:
617 >>> loader = ModuleLoader('/path/to/compiled/templates')
619 Templates can be precompiled with :meth:`Environment.compile_templates`.
620 """
622 has_source_access = False
624 def __init__(
625 self,
626 path: t.Union[
627 str, "os.PathLike[str]", t.Sequence[t.Union[str, "os.PathLike[str]"]]
628 ],
629 ) -> None:
630 package_name = f"_jinja2_module_templates_{id(self):x}"
632 # create a fake module that looks for the templates in the
633 # path given.
634 mod = _TemplateModule(package_name)
636 if not isinstance(path, abc.Iterable) or isinstance(path, str):
637 path = [path]
639 mod.__path__ = [os.fspath(p) for p in path]
641 sys.modules[package_name] = weakref.proxy(
642 mod, lambda x: sys.modules.pop(package_name, None)
643 )
645 # the only strong reference, the sys.modules entry is weak
646 # so that the garbage collector can remove it once the
647 # loader that created it goes out of business.
648 self.module = mod
649 self.package_name = package_name
651 @staticmethod
652 def get_template_key(name: str) -> str:
653 return "tmpl_" + sha1(name.encode("utf-8")).hexdigest()
655 @staticmethod
656 def get_module_filename(name: str) -> str:
657 return ModuleLoader.get_template_key(name) + ".py"
659 @internalcode
660 def load(
661 self,
662 environment: "Environment",
663 name: str,
664 globals: t.MutableMapping[str, t.Any] | None = None,
665 ) -> "Template":
666 key = self.get_template_key(name)
667 module = f"{self.package_name}.{key}"
668 mod = getattr(self.module, module, None)
670 if mod is None:
671 try:
672 mod = __import__(module, None, None, ["root"])
673 except ImportError as e:
674 raise TemplateNotFound(name) from e
676 # remove the entry from sys.modules, we only want the attribute
677 # on the module object we have stored on the loader.
678 sys.modules.pop(module, None)
680 if globals is None:
681 globals = {}
683 return environment.template_class.from_module_dict(
684 environment, mod.__dict__, globals
685 )