Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/jinja2/loaders.py: 30%
258 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
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
18from .utils import open_if_exists
20if t.TYPE_CHECKING: 20 ↛ 21line 20 didn't jump to line 21, because the condition on line 20 was never true
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.path.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[str, os.PathLike, t.Sequence[t.Union[str, os.PathLike]]],
182 encoding: str = "utf-8",
183 followlinks: bool = False,
184 ) -> None:
185 if not isinstance(searchpath, abc.Iterable) or isinstance(searchpath, str):
186 searchpath = [searchpath]
188 self.searchpath = [os.fspath(p) for p in searchpath]
189 self.encoding = encoding
190 self.followlinks = followlinks
192 def get_source(
193 self, environment: "Environment", template: str
194 ) -> t.Tuple[str, str, t.Callable[[], bool]]:
195 pieces = split_template_path(template)
196 for searchpath in self.searchpath:
197 # Use posixpath even on Windows to avoid "drive:" or UNC
198 # segments breaking out of the search directory.
199 filename = posixpath.join(searchpath, *pieces)
200 f = open_if_exists(filename)
201 if f is None:
202 continue
203 try:
204 contents = f.read().decode(self.encoding)
205 finally:
206 f.close()
208 mtime = os.path.getmtime(filename)
210 def uptodate() -> bool:
211 try:
212 return os.path.getmtime(filename) == mtime
213 except OSError:
214 return False
216 # Use normpath to convert Windows altsep to sep.
217 return contents, os.path.normpath(filename), uptodate
218 raise TemplateNotFound(template)
220 def list_templates(self) -> t.List[str]:
221 found = set()
222 for searchpath in self.searchpath:
223 walk_dir = os.walk(searchpath, followlinks=self.followlinks)
224 for dirpath, _, filenames in walk_dir:
225 for filename in filenames:
226 template = (
227 os.path.join(dirpath, filename)[len(searchpath) :]
228 .strip(os.path.sep)
229 .replace(os.path.sep, "/")
230 )
231 if template[:2] == "./":
232 template = template[2:]
233 if template not in found:
234 found.add(template)
235 return sorted(found)
238class PackageLoader(BaseLoader):
239 """Load templates from a directory in a Python package.
241 :param package_name: Import name of the package that contains the
242 template directory.
243 :param package_path: Directory within the imported package that
244 contains the templates.
245 :param encoding: Encoding of template files.
247 The following example looks up templates in the ``pages`` directory
248 within the ``project.ui`` package.
250 .. code-block:: python
252 loader = PackageLoader("project.ui", "pages")
254 Only packages installed as directories (standard pip behavior) or
255 zip/egg files (less common) are supported. The Python API for
256 introspecting data in packages is too limited to support other
257 installation methods the way this loader requires.
259 There is limited support for :pep:`420` namespace packages. The
260 template directory is assumed to only be in one namespace
261 contributor. Zip files contributing to a namespace are not
262 supported.
264 .. versionchanged:: 3.0
265 No longer uses ``setuptools`` as a dependency.
267 .. versionchanged:: 3.0
268 Limited PEP 420 namespace package support.
269 """
271 def __init__(
272 self,
273 package_name: str,
274 package_path: "str" = "templates",
275 encoding: str = "utf-8",
276 ) -> None:
277 package_path = os.path.normpath(package_path).rstrip(os.path.sep)
279 # normpath preserves ".", which isn't valid in zip paths.
280 if package_path == os.path.curdir: 280 ↛ 281line 280 didn't jump to line 281, because the condition on line 280 was never true
281 package_path = ""
282 elif package_path[:2] == os.path.curdir + os.path.sep: 282 ↛ 283line 282 didn't jump to line 283, because the condition on line 282 was never true
283 package_path = package_path[2:]
285 self.package_path = package_path
286 self.package_name = package_name
287 self.encoding = encoding
289 # Make sure the package exists. This also makes namespace
290 # packages work, otherwise get_loader returns None.
291 import_module(package_name)
292 spec = importlib.util.find_spec(package_name)
293 assert spec is not None, "An import spec was not found for the package."
294 loader = spec.loader
295 assert loader is not None, "A loader was not found for the package."
296 self._loader = loader
297 self._archive = None
298 template_root = None
300 if isinstance(loader, zipimport.zipimporter): 300 ↛ 301line 300 didn't jump to line 301, because the condition on line 300 was never true
301 self._archive = loader.archive
302 pkgdir = next(iter(spec.submodule_search_locations)) # type: ignore
303 template_root = os.path.join(pkgdir, package_path).rstrip(os.path.sep)
304 else:
305 roots: t.List[str] = []
307 # One element for regular packages, multiple for namespace
308 # packages, or None for single module file.
309 if spec.submodule_search_locations: 309 ↛ 312line 309 didn't jump to line 312, because the condition on line 309 was never false
310 roots.extend(spec.submodule_search_locations)
311 # A single module file, use the parent directory instead.
312 elif spec.origin is not None:
313 roots.append(os.path.dirname(spec.origin))
315 for root in roots: 315 ↛ 322line 315 didn't jump to line 322, because the loop on line 315 didn't complete
316 root = os.path.join(root, package_path)
318 if os.path.isdir(root): 318 ↛ 315line 318 didn't jump to line 315, because the condition on line 318 was never false
319 template_root = root
320 break
322 if template_root is None: 322 ↛ 323line 322 didn't jump to line 323, because the condition on line 322 was never true
323 raise ValueError(
324 f"The {package_name!r} package was not installed in a"
325 " way that PackageLoader understands."
326 )
328 self._template_root = template_root
330 def get_source(
331 self, environment: "Environment", template: str
332 ) -> t.Tuple[str, str, t.Optional[t.Callable[[], bool]]]:
333 # Use posixpath even on Windows to avoid "drive:" or UNC
334 # segments breaking out of the search directory. Use normpath to
335 # convert Windows altsep to sep.
336 p = os.path.normpath(
337 posixpath.join(self._template_root, *split_template_path(template))
338 )
339 up_to_date: t.Optional[t.Callable[[], bool]]
341 if self._archive is None:
342 # Package is a directory.
343 if not os.path.isfile(p):
344 raise TemplateNotFound(template)
346 with open(p, "rb") as f:
347 source = f.read()
349 mtime = os.path.getmtime(p)
351 def up_to_date() -> bool:
352 return os.path.isfile(p) and os.path.getmtime(p) == mtime
354 else:
355 # Package is a zip file.
356 try:
357 source = self._loader.get_data(p) # type: ignore
358 except OSError as e:
359 raise TemplateNotFound(template) from e
361 # Could use the zip's mtime for all template mtimes, but
362 # would need to safely reload the module if it's out of
363 # date, so just report it as always current.
364 up_to_date = None
366 return source.decode(self.encoding), p, up_to_date
368 def list_templates(self) -> t.List[str]:
369 results: t.List[str] = []
371 if self._archive is None:
372 # Package is a directory.
373 offset = len(self._template_root)
375 for dirpath, _, filenames in os.walk(self._template_root):
376 dirpath = dirpath[offset:].lstrip(os.path.sep)
377 results.extend(
378 os.path.join(dirpath, name).replace(os.path.sep, "/")
379 for name in filenames
380 )
381 else:
382 if not hasattr(self._loader, "_files"):
383 raise TypeError(
384 "This zip import does not have the required"
385 " metadata to list templates."
386 )
388 # Package is a zip file.
389 prefix = (
390 self._template_root[len(self._archive) :].lstrip(os.path.sep)
391 + os.path.sep
392 )
393 offset = len(prefix)
395 for name in self._loader._files.keys(): # type: ignore
396 # Find names under the templates directory that aren't directories.
397 if name.startswith(prefix) and name[-1] != os.path.sep:
398 results.append(name[offset:].replace(os.path.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, path: t.Union[str, os.PathLike, t.Sequence[t.Union[str, os.PathLike]]]
605 ) -> None:
606 package_name = f"_jinja2_module_templates_{id(self):x}"
608 # create a fake module that looks for the templates in the
609 # path given.
610 mod = _TemplateModule(package_name)
612 if not isinstance(path, abc.Iterable) or isinstance(path, str):
613 path = [path]
615 mod.__path__ = [os.fspath(p) for p in path]
617 sys.modules[package_name] = weakref.proxy(
618 mod, lambda x: sys.modules.pop(package_name, None)
619 )
621 # the only strong reference, the sys.modules entry is weak
622 # so that the garbage collector can remove it once the
623 # loader that created it goes out of business.
624 self.module = mod
625 self.package_name = package_name
627 @staticmethod
628 def get_template_key(name: str) -> str:
629 return "tmpl_" + sha1(name.encode("utf-8")).hexdigest()
631 @staticmethod
632 def get_module_filename(name: str) -> str:
633 return ModuleLoader.get_template_key(name) + ".py"
635 @internalcode
636 def load(
637 self,
638 environment: "Environment",
639 name: str,
640 globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
641 ) -> "Template":
642 key = self.get_template_key(name)
643 module = f"{self.package_name}.{key}"
644 mod = getattr(self.module, module, None)
646 if mod is None:
647 try:
648 mod = __import__(module, None, None, ["root"])
649 except ImportError as e:
650 raise TemplateNotFound(name) from e
652 # remove the entry from sys.modules, we only want the attribute
653 # on the module object we have stored on the loader.
654 sys.modules.pop(module, None)
656 if globals is None:
657 globals = {}
659 return environment.template_class.from_module_dict(
660 environment, mod.__dict__, globals
661 )