• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""API and implementations for loading templates from different data
2sources.
3"""
4import importlib.util
5import os
6import sys
7import weakref
8import zipimport
9from collections import abc
10from hashlib import sha1
11from importlib import import_module
12from types import ModuleType
13
14from .exceptions import TemplateNotFound
15from .utils import internalcode
16from .utils import open_if_exists
17
18
19def split_template_path(template):
20    """Split a path into segments and perform a sanity check.  If it detects
21    '..' in the path it will raise a `TemplateNotFound` error.
22    """
23    pieces = []
24    for piece in template.split("/"):
25        if (
26            os.path.sep in piece
27            or (os.path.altsep and os.path.altsep in piece)
28            or piece == os.path.pardir
29        ):
30            raise TemplateNotFound(template)
31        elif piece and piece != ".":
32            pieces.append(piece)
33    return pieces
34
35
36class BaseLoader:
37    """Baseclass for all loaders.  Subclass this and override `get_source` to
38    implement a custom loading mechanism.  The environment provides a
39    `get_template` method that calls the loader's `load` method to get the
40    :class:`Template` object.
41
42    A very basic example for a loader that looks up templates on the file
43    system could look like this::
44
45        from jinja2 import BaseLoader, TemplateNotFound
46        from os.path import join, exists, getmtime
47
48        class MyLoader(BaseLoader):
49
50            def __init__(self, path):
51                self.path = path
52
53            def get_source(self, environment, template):
54                path = join(self.path, template)
55                if not exists(path):
56                    raise TemplateNotFound(template)
57                mtime = getmtime(path)
58                with open(path) as f:
59                    source = f.read()
60                return source, path, lambda: mtime == getmtime(path)
61    """
62
63    #: if set to `False` it indicates that the loader cannot provide access
64    #: to the source of templates.
65    #:
66    #: .. versionadded:: 2.4
67    has_source_access = True
68
69    def get_source(self, environment, template):
70        """Get the template source, filename and reload helper for a template.
71        It's passed the environment and template name and has to return a
72        tuple in the form ``(source, filename, uptodate)`` or raise a
73        `TemplateNotFound` error if it can't locate the template.
74
75        The source part of the returned tuple must be the source of the
76        template as a string. The filename should be the name of the
77        file on the filesystem if it was loaded from there, otherwise
78        ``None``. The filename is used by Python for the tracebacks
79        if no loader extension is used.
80
81        The last item in the tuple is the `uptodate` function.  If auto
82        reloading is enabled it's always called to check if the template
83        changed.  No arguments are passed so the function must store the
84        old state somewhere (for example in a closure).  If it returns `False`
85        the template will be reloaded.
86        """
87        if not self.has_source_access:
88            raise RuntimeError(
89                f"{self.__class__.__name__} cannot provide access to the source"
90            )
91        raise TemplateNotFound(template)
92
93    def list_templates(self):
94        """Iterates over all templates.  If the loader does not support that
95        it should raise a :exc:`TypeError` which is the default behavior.
96        """
97        raise TypeError("this loader cannot iterate over all templates")
98
99    @internalcode
100    def load(self, environment, name, globals=None):
101        """Loads a template.  This method looks up the template in the cache
102        or loads one by calling :meth:`get_source`.  Subclasses should not
103        override this method as loaders working on collections of other
104        loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`)
105        will not call this method but `get_source` directly.
106        """
107        code = None
108        if globals is None:
109            globals = {}
110
111        # first we try to get the source for this template together
112        # with the filename and the uptodate function.
113        source, filename, uptodate = self.get_source(environment, name)
114
115        # try to load the code from the bytecode cache if there is a
116        # bytecode cache configured.
117        bcc = environment.bytecode_cache
118        if bcc is not None:
119            bucket = bcc.get_bucket(environment, name, filename, source)
120            code = bucket.code
121
122        # if we don't have code so far (not cached, no longer up to
123        # date) etc. we compile the template
124        if code is None:
125            code = environment.compile(source, name, filename)
126
127        # if the bytecode cache is available and the bucket doesn't
128        # have a code so far, we give the bucket the new code and put
129        # it back to the bytecode cache.
130        if bcc is not None and bucket.code is None:
131            bucket.code = code
132            bcc.set_bucket(bucket)
133
134        return environment.template_class.from_code(
135            environment, code, globals, uptodate
136        )
137
138
139class FileSystemLoader(BaseLoader):
140    """Load templates from a directory in the file system.
141
142    The path can be relative or absolute. Relative paths are relative to
143    the current working directory.
144
145    .. code-block:: python
146
147        loader = FileSystemLoader("templates")
148
149    A list of paths can be given. The directories will be searched in
150    order, stopping at the first matching template.
151
152    .. code-block:: python
153
154        loader = FileSystemLoader(["/override/templates", "/default/templates"])
155
156    :param searchpath: A path, or list of paths, to the directory that
157        contains the templates.
158    :param encoding: Use this encoding to read the text from template
159        files.
160    :param followlinks: Follow symbolic links in the path.
161
162    .. versionchanged:: 2.8
163        Added the ``followlinks`` parameter.
164    """
165
166    def __init__(self, searchpath, encoding="utf-8", followlinks=False):
167        if not isinstance(searchpath, abc.Iterable) or isinstance(searchpath, str):
168            searchpath = [searchpath]
169
170        self.searchpath = list(searchpath)
171        self.encoding = encoding
172        self.followlinks = followlinks
173
174    def get_source(self, environment, template):
175        pieces = split_template_path(template)
176        for searchpath in self.searchpath:
177            filename = os.path.join(searchpath, *pieces)
178            f = open_if_exists(filename)
179            if f is None:
180                continue
181            try:
182                contents = f.read().decode(self.encoding)
183            finally:
184                f.close()
185
186            mtime = os.path.getmtime(filename)
187
188            def uptodate():
189                try:
190                    return os.path.getmtime(filename) == mtime
191                except OSError:
192                    return False
193
194            return contents, filename, uptodate
195        raise TemplateNotFound(template)
196
197    def list_templates(self):
198        found = set()
199        for searchpath in self.searchpath:
200            walk_dir = os.walk(searchpath, followlinks=self.followlinks)
201            for dirpath, _, filenames in walk_dir:
202                for filename in filenames:
203                    template = (
204                        os.path.join(dirpath, filename)[len(searchpath) :]
205                        .strip(os.path.sep)
206                        .replace(os.path.sep, "/")
207                    )
208                    if template[:2] == "./":
209                        template = template[2:]
210                    if template not in found:
211                        found.add(template)
212        return sorted(found)
213
214
215class PackageLoader(BaseLoader):
216    """Load templates from a directory in a Python package.
217
218    :param package_name: Import name of the package that contains the
219        template directory.
220    :param package_path: Directory within the imported package that
221        contains the templates.
222    :param encoding: Encoding of template files.
223
224    The following example looks up templates in the ``pages`` directory
225    within the ``project.ui`` package.
226
227    .. code-block:: python
228
229        loader = PackageLoader("project.ui", "pages")
230
231    Only packages installed as directories (standard pip behavior) or
232    zip/egg files (less common) are supported. The Python API for
233    introspecting data in packages is too limited to support other
234    installation methods the way this loader requires.
235
236    There is limited support for :pep:`420` namespace packages. The
237    template directory is assumed to only be in one namespace
238    contributor. Zip files contributing to a namespace are not
239    supported.
240
241    .. versionchanged:: 3.0
242        No longer uses ``setuptools`` as a dependency.
243
244    .. versionchanged:: 3.0
245        Limited PEP 420 namespace package support.
246    """
247
248    def __init__(self, package_name, package_path="templates", encoding="utf-8"):
249        if package_path == os.path.curdir:
250            package_path = ""
251        elif package_path[:2] == os.path.curdir + os.path.sep:
252            package_path = package_path[2:]
253
254        package_path = os.path.normpath(package_path).rstrip(os.path.sep)
255        self.package_path = package_path
256        self.package_name = package_name
257        self.encoding = encoding
258
259        # Make sure the package exists. This also makes namespace
260        # packages work, otherwise get_loader returns None.
261        import_module(package_name)
262        spec = importlib.util.find_spec(package_name)
263        self._loader = loader = spec.loader
264        self._archive = None
265        self._template_root = None
266
267        if isinstance(loader, zipimport.zipimporter):
268            self._archive = loader.archive
269            pkgdir = next(iter(spec.submodule_search_locations))
270            self._template_root = os.path.join(pkgdir, package_path)
271        elif spec.submodule_search_locations:
272            # This will be one element for regular packages and multiple
273            # for namespace packages.
274            for root in spec.submodule_search_locations:
275                root = os.path.join(root, package_path)
276
277                if os.path.isdir(root):
278                    self._template_root = root
279                    break
280
281        if self._template_root is None:
282            raise ValueError(
283                f"The {package_name!r} package was not installed in a"
284                " way that PackageLoader understands."
285            )
286
287    def get_source(self, environment, template):
288        p = os.path.join(self._template_root, *split_template_path(template))
289
290        if self._archive is None:
291            # Package is a directory.
292            if not os.path.isfile(p):
293                raise TemplateNotFound(template)
294
295            with open(p, "rb") as f:
296                source = f.read()
297
298            mtime = os.path.getmtime(p)
299
300            def up_to_date():
301                return os.path.isfile(p) and os.path.getmtime(p) == mtime
302
303        else:
304            # Package is a zip file.
305            try:
306                source = self._loader.get_data(p)
307            except OSError:
308                raise TemplateNotFound(template)
309
310            # Could use the zip's mtime for all template mtimes, but
311            # would need to safely reload the module if it's out of
312            # date, so just report it as always current.
313            up_to_date = None
314
315        return source.decode(self.encoding), p, up_to_date
316
317    def list_templates(self):
318        results = []
319
320        if self._archive is None:
321            # Package is a directory.
322            offset = len(self._template_root)
323
324            for dirpath, _, filenames in os.walk(self._template_root):
325                dirpath = dirpath[offset:].lstrip(os.path.sep)
326                results.extend(
327                    os.path.join(dirpath, name).replace(os.path.sep, "/")
328                    for name in filenames
329                )
330        else:
331            if not hasattr(self._loader, "_files"):
332                raise TypeError(
333                    "This zip import does not have the required"
334                    " metadata to list templates."
335                )
336
337            # Package is a zip file.
338            prefix = (
339                self._template_root[len(self._archive) :].lstrip(os.path.sep)
340                + os.path.sep
341            )
342            offset = len(prefix)
343
344            for name in self._loader._files.keys():
345                # Find names under the templates directory that aren't directories.
346                if name.startswith(prefix) and name[-1] != os.path.sep:
347                    results.append(name[offset:].replace(os.path.sep, "/"))
348
349        results.sort()
350        return results
351
352
353class DictLoader(BaseLoader):
354    """Loads a template from a Python dict mapping template names to
355    template source.  This loader is useful for unittesting:
356
357    >>> loader = DictLoader({'index.html': 'source here'})
358
359    Because auto reloading is rarely useful this is disabled per default.
360    """
361
362    def __init__(self, mapping):
363        self.mapping = mapping
364
365    def get_source(self, environment, template):
366        if template in self.mapping:
367            source = self.mapping[template]
368            return source, None, lambda: source == self.mapping.get(template)
369        raise TemplateNotFound(template)
370
371    def list_templates(self):
372        return sorted(self.mapping)
373
374
375class FunctionLoader(BaseLoader):
376    """A loader that is passed a function which does the loading.  The
377    function receives the name of the template and has to return either
378    a string with the template source, a tuple in the form ``(source,
379    filename, uptodatefunc)`` or `None` if the template does not exist.
380
381    >>> def load_template(name):
382    ...     if name == 'index.html':
383    ...         return '...'
384    ...
385    >>> loader = FunctionLoader(load_template)
386
387    The `uptodatefunc` is a function that is called if autoreload is enabled
388    and has to return `True` if the template is still up to date.  For more
389    details have a look at :meth:`BaseLoader.get_source` which has the same
390    return value.
391    """
392
393    def __init__(self, load_func):
394        self.load_func = load_func
395
396    def get_source(self, environment, template):
397        rv = self.load_func(template)
398        if rv is None:
399            raise TemplateNotFound(template)
400        elif isinstance(rv, str):
401            return rv, None, None
402        return rv
403
404
405class PrefixLoader(BaseLoader):
406    """A loader that is passed a dict of loaders where each loader is bound
407    to a prefix.  The prefix is delimited from the template by a slash per
408    default, which can be changed by setting the `delimiter` argument to
409    something else::
410
411        loader = PrefixLoader({
412            'app1':     PackageLoader('mypackage.app1'),
413            'app2':     PackageLoader('mypackage.app2')
414        })
415
416    By loading ``'app1/index.html'`` the file from the app1 package is loaded,
417    by loading ``'app2/index.html'`` the file from the second.
418    """
419
420    def __init__(self, mapping, delimiter="/"):
421        self.mapping = mapping
422        self.delimiter = delimiter
423
424    def get_loader(self, template):
425        try:
426            prefix, name = template.split(self.delimiter, 1)
427            loader = self.mapping[prefix]
428        except (ValueError, KeyError):
429            raise TemplateNotFound(template)
430        return loader, name
431
432    def get_source(self, environment, template):
433        loader, name = self.get_loader(template)
434        try:
435            return loader.get_source(environment, name)
436        except TemplateNotFound:
437            # re-raise the exception with the correct filename here.
438            # (the one that includes the prefix)
439            raise TemplateNotFound(template)
440
441    @internalcode
442    def load(self, environment, name, globals=None):
443        loader, local_name = self.get_loader(name)
444        try:
445            return loader.load(environment, local_name, globals)
446        except TemplateNotFound:
447            # re-raise the exception with the correct filename here.
448            # (the one that includes the prefix)
449            raise TemplateNotFound(name)
450
451    def list_templates(self):
452        result = []
453        for prefix, loader in self.mapping.items():
454            for template in loader.list_templates():
455                result.append(prefix + self.delimiter + template)
456        return result
457
458
459class ChoiceLoader(BaseLoader):
460    """This loader works like the `PrefixLoader` just that no prefix is
461    specified.  If a template could not be found by one loader the next one
462    is tried.
463
464    >>> loader = ChoiceLoader([
465    ...     FileSystemLoader('/path/to/user/templates'),
466    ...     FileSystemLoader('/path/to/system/templates')
467    ... ])
468
469    This is useful if you want to allow users to override builtin templates
470    from a different location.
471    """
472
473    def __init__(self, loaders):
474        self.loaders = loaders
475
476    def get_source(self, environment, template):
477        for loader in self.loaders:
478            try:
479                return loader.get_source(environment, template)
480            except TemplateNotFound:
481                pass
482        raise TemplateNotFound(template)
483
484    @internalcode
485    def load(self, environment, name, globals=None):
486        for loader in self.loaders:
487            try:
488                return loader.load(environment, name, globals)
489            except TemplateNotFound:
490                pass
491        raise TemplateNotFound(name)
492
493    def list_templates(self):
494        found = set()
495        for loader in self.loaders:
496            found.update(loader.list_templates())
497        return sorted(found)
498
499
500class _TemplateModule(ModuleType):
501    """Like a normal module but with support for weak references"""
502
503
504class ModuleLoader(BaseLoader):
505    """This loader loads templates from precompiled templates.
506
507    Example usage:
508
509    >>> loader = ChoiceLoader([
510    ...     ModuleLoader('/path/to/compiled/templates'),
511    ...     FileSystemLoader('/path/to/templates')
512    ... ])
513
514    Templates can be precompiled with :meth:`Environment.compile_templates`.
515    """
516
517    has_source_access = False
518
519    def __init__(self, path):
520        package_name = f"_jinja2_module_templates_{id(self):x}"
521
522        # create a fake module that looks for the templates in the
523        # path given.
524        mod = _TemplateModule(package_name)
525
526        if not isinstance(path, abc.Iterable) or isinstance(path, str):
527            path = [path]
528
529        mod.__path__ = [os.fspath(p) for p in path]
530
531        sys.modules[package_name] = weakref.proxy(
532            mod, lambda x: sys.modules.pop(package_name, None)
533        )
534
535        # the only strong reference, the sys.modules entry is weak
536        # so that the garbage collector can remove it once the
537        # loader that created it goes out of business.
538        self.module = mod
539        self.package_name = package_name
540
541    @staticmethod
542    def get_template_key(name):
543        return "tmpl_" + sha1(name.encode("utf-8")).hexdigest()
544
545    @staticmethod
546    def get_module_filename(name):
547        return ModuleLoader.get_template_key(name) + ".py"
548
549    @internalcode
550    def load(self, environment, name, globals=None):
551        key = self.get_template_key(name)
552        module = f"{self.package_name}.{key}"
553        mod = getattr(self.module, module, None)
554        if mod is None:
555            try:
556                mod = __import__(module, None, None, ["root"])
557            except ImportError:
558                raise TemplateNotFound(name)
559
560            # remove the entry from sys.modules, we only want the attribute
561            # on the module object we have stored on the loader.
562            sys.modules.pop(module, None)
563
564        return environment.template_class.from_module_dict(
565            environment, mod.__dict__, globals
566        )
567