diff options
Diffstat (limited to 'venv/lib/python3.11/site-packages/jinja2/loaders.py')
| -rw-r--r-- | venv/lib/python3.11/site-packages/jinja2/loaders.py | 661 | 
1 files changed, 661 insertions, 0 deletions
| diff --git a/venv/lib/python3.11/site-packages/jinja2/loaders.py b/venv/lib/python3.11/site-packages/jinja2/loaders.py new file mode 100644 index 0000000..32f3a74 --- /dev/null +++ b/venv/lib/python3.11/site-packages/jinja2/loaders.py @@ -0,0 +1,661 @@ +"""API and implementations for loading templates from different data +sources. +""" +import importlib.util +import os +import posixpath +import sys +import typing as t +import weakref +import zipimport +from collections import abc +from hashlib import sha1 +from importlib import import_module +from types import ModuleType + +from .exceptions import TemplateNotFound +from .utils import internalcode + +if t.TYPE_CHECKING: +    from .environment import Environment +    from .environment import Template + + +def split_template_path(template: str) -> t.List[str]: +    """Split a path into segments and perform a sanity check.  If it detects +    '..' in the path it will raise a `TemplateNotFound` error. +    """ +    pieces = [] +    for piece in template.split("/"): +        if ( +            os.path.sep in piece +            or (os.path.altsep and os.path.altsep in piece) +            or piece == os.path.pardir +        ): +            raise TemplateNotFound(template) +        elif piece and piece != ".": +            pieces.append(piece) +    return pieces + + +class BaseLoader: +    """Baseclass for all loaders.  Subclass this and override `get_source` to +    implement a custom loading mechanism.  The environment provides a +    `get_template` method that calls the loader's `load` method to get the +    :class:`Template` object. + +    A very basic example for a loader that looks up templates on the file +    system could look like this:: + +        from jinja2 import BaseLoader, TemplateNotFound +        from os.path import join, exists, getmtime + +        class MyLoader(BaseLoader): + +            def __init__(self, path): +                self.path = path + +            def get_source(self, environment, template): +                path = join(self.path, template) +                if not exists(path): +                    raise TemplateNotFound(template) +                mtime = getmtime(path) +                with open(path) as f: +                    source = f.read() +                return source, path, lambda: mtime == getmtime(path) +    """ + +    #: if set to `False` it indicates that the loader cannot provide access +    #: to the source of templates. +    #: +    #: .. versionadded:: 2.4 +    has_source_access = True + +    def get_source( +        self, environment: "Environment", template: str +    ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]: +        """Get the template source, filename and reload helper for a template. +        It's passed the environment and template name and has to return a +        tuple in the form ``(source, filename, uptodate)`` or raise a +        `TemplateNotFound` error if it can't locate the template. + +        The source part of the returned tuple must be the source of the +        template as a string. The filename should be the name of the +        file on the filesystem if it was loaded from there, otherwise +        ``None``. The filename is used by Python for the tracebacks +        if no loader extension is used. + +        The last item in the tuple is the `uptodate` function.  If auto +        reloading is enabled it's always called to check if the template +        changed.  No arguments are passed so the function must store the +        old state somewhere (for example in a closure).  If it returns `False` +        the template will be reloaded. +        """ +        if not self.has_source_access: +            raise RuntimeError( +                f"{type(self).__name__} cannot provide access to the source" +            ) +        raise TemplateNotFound(template) + +    def list_templates(self) -> t.List[str]: +        """Iterates over all templates.  If the loader does not support that +        it should raise a :exc:`TypeError` which is the default behavior. +        """ +        raise TypeError("this loader cannot iterate over all templates") + +    @internalcode +    def load( +        self, +        environment: "Environment", +        name: str, +        globals: t.Optional[t.MutableMapping[str, t.Any]] = None, +    ) -> "Template": +        """Loads a template.  This method looks up the template in the cache +        or loads one by calling :meth:`get_source`.  Subclasses should not +        override this method as loaders working on collections of other +        loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`) +        will not call this method but `get_source` directly. +        """ +        code = None +        if globals is None: +            globals = {} + +        # first we try to get the source for this template together +        # with the filename and the uptodate function. +        source, filename, uptodate = self.get_source(environment, name) + +        # try to load the code from the bytecode cache if there is a +        # bytecode cache configured. +        bcc = environment.bytecode_cache +        if bcc is not None: +            bucket = bcc.get_bucket(environment, name, filename, source) +            code = bucket.code + +        # if we don't have code so far (not cached, no longer up to +        # date) etc. we compile the template +        if code is None: +            code = environment.compile(source, name, filename) + +        # if the bytecode cache is available and the bucket doesn't +        # have a code so far, we give the bucket the new code and put +        # it back to the bytecode cache. +        if bcc is not None and bucket.code is None: +            bucket.code = code +            bcc.set_bucket(bucket) + +        return environment.template_class.from_code( +            environment, code, globals, uptodate +        ) + + +class FileSystemLoader(BaseLoader): +    """Load templates from a directory in the file system. + +    The path can be relative or absolute. Relative paths are relative to +    the current working directory. + +    .. code-block:: python + +        loader = FileSystemLoader("templates") + +    A list of paths can be given. The directories will be searched in +    order, stopping at the first matching template. + +    .. code-block:: python + +        loader = FileSystemLoader(["/override/templates", "/default/templates"]) + +    :param searchpath: A path, or list of paths, to the directory that +        contains the templates. +    :param encoding: Use this encoding to read the text from template +        files. +    :param followlinks: Follow symbolic links in the path. + +    .. versionchanged:: 2.8 +        Added the ``followlinks`` parameter. +    """ + +    def __init__( +        self, +        searchpath: t.Union[str, os.PathLike, t.Sequence[t.Union[str, os.PathLike]]], +        encoding: str = "utf-8", +        followlinks: bool = False, +    ) -> None: +        if not isinstance(searchpath, abc.Iterable) or isinstance(searchpath, str): +            searchpath = [searchpath] + +        self.searchpath = [os.fspath(p) for p in searchpath] +        self.encoding = encoding +        self.followlinks = followlinks + +    def get_source( +        self, environment: "Environment", template: str +    ) -> t.Tuple[str, str, t.Callable[[], bool]]: +        pieces = split_template_path(template) + +        for searchpath in self.searchpath: +            # Use posixpath even on Windows to avoid "drive:" or UNC +            # segments breaking out of the search directory. +            filename = posixpath.join(searchpath, *pieces) + +            if os.path.isfile(filename): +                break +        else: +            raise TemplateNotFound(template) + +        with open(filename, encoding=self.encoding) as f: +            contents = f.read() + +        mtime = os.path.getmtime(filename) + +        def uptodate() -> bool: +            try: +                return os.path.getmtime(filename) == mtime +            except OSError: +                return False + +        # Use normpath to convert Windows altsep to sep. +        return contents, os.path.normpath(filename), uptodate + +    def list_templates(self) -> t.List[str]: +        found = set() +        for searchpath in self.searchpath: +            walk_dir = os.walk(searchpath, followlinks=self.followlinks) +            for dirpath, _, filenames in walk_dir: +                for filename in filenames: +                    template = ( +                        os.path.join(dirpath, filename)[len(searchpath) :] +                        .strip(os.path.sep) +                        .replace(os.path.sep, "/") +                    ) +                    if template[:2] == "./": +                        template = template[2:] +                    if template not in found: +                        found.add(template) +        return sorted(found) + + +class PackageLoader(BaseLoader): +    """Load templates from a directory in a Python package. + +    :param package_name: Import name of the package that contains the +        template directory. +    :param package_path: Directory within the imported package that +        contains the templates. +    :param encoding: Encoding of template files. + +    The following example looks up templates in the ``pages`` directory +    within the ``project.ui`` package. + +    .. code-block:: python + +        loader = PackageLoader("project.ui", "pages") + +    Only packages installed as directories (standard pip behavior) or +    zip/egg files (less common) are supported. The Python API for +    introspecting data in packages is too limited to support other +    installation methods the way this loader requires. + +    There is limited support for :pep:`420` namespace packages. The +    template directory is assumed to only be in one namespace +    contributor. Zip files contributing to a namespace are not +    supported. + +    .. versionchanged:: 3.0 +        No longer uses ``setuptools`` as a dependency. + +    .. versionchanged:: 3.0 +        Limited PEP 420 namespace package support. +    """ + +    def __init__( +        self, +        package_name: str, +        package_path: "str" = "templates", +        encoding: str = "utf-8", +    ) -> None: +        package_path = os.path.normpath(package_path).rstrip(os.path.sep) + +        # normpath preserves ".", which isn't valid in zip paths. +        if package_path == os.path.curdir: +            package_path = "" +        elif package_path[:2] == os.path.curdir + os.path.sep: +            package_path = package_path[2:] + +        self.package_path = package_path +        self.package_name = package_name +        self.encoding = encoding + +        # Make sure the package exists. This also makes namespace +        # packages work, otherwise get_loader returns None. +        import_module(package_name) +        spec = importlib.util.find_spec(package_name) +        assert spec is not None, "An import spec was not found for the package." +        loader = spec.loader +        assert loader is not None, "A loader was not found for the package." +        self._loader = loader +        self._archive = None +        template_root = None + +        if isinstance(loader, zipimport.zipimporter): +            self._archive = loader.archive +            pkgdir = next(iter(spec.submodule_search_locations))  # type: ignore +            template_root = os.path.join(pkgdir, package_path).rstrip(os.path.sep) +        else: +            roots: t.List[str] = [] + +            # One element for regular packages, multiple for namespace +            # packages, or None for single module file. +            if spec.submodule_search_locations: +                roots.extend(spec.submodule_search_locations) +            # A single module file, use the parent directory instead. +            elif spec.origin is not None: +                roots.append(os.path.dirname(spec.origin)) + +            for root in roots: +                root = os.path.join(root, package_path) + +                if os.path.isdir(root): +                    template_root = root +                    break + +        if template_root is None: +            raise ValueError( +                f"The {package_name!r} package was not installed in a" +                " way that PackageLoader understands." +            ) + +        self._template_root = template_root + +    def get_source( +        self, environment: "Environment", template: str +    ) -> t.Tuple[str, str, t.Optional[t.Callable[[], bool]]]: +        # Use posixpath even on Windows to avoid "drive:" or UNC +        # segments breaking out of the search directory. Use normpath to +        # convert Windows altsep to sep. +        p = os.path.normpath( +            posixpath.join(self._template_root, *split_template_path(template)) +        ) +        up_to_date: t.Optional[t.Callable[[], bool]] + +        if self._archive is None: +            # Package is a directory. +            if not os.path.isfile(p): +                raise TemplateNotFound(template) + +            with open(p, "rb") as f: +                source = f.read() + +            mtime = os.path.getmtime(p) + +            def up_to_date() -> bool: +                return os.path.isfile(p) and os.path.getmtime(p) == mtime + +        else: +            # Package is a zip file. +            try: +                source = self._loader.get_data(p)  # type: ignore +            except OSError as e: +                raise TemplateNotFound(template) from e + +            # Could use the zip's mtime for all template mtimes, but +            # would need to safely reload the module if it's out of +            # date, so just report it as always current. +            up_to_date = None + +        return source.decode(self.encoding), p, up_to_date + +    def list_templates(self) -> t.List[str]: +        results: t.List[str] = [] + +        if self._archive is None: +            # Package is a directory. +            offset = len(self._template_root) + +            for dirpath, _, filenames in os.walk(self._template_root): +                dirpath = dirpath[offset:].lstrip(os.path.sep) +                results.extend( +                    os.path.join(dirpath, name).replace(os.path.sep, "/") +                    for name in filenames +                ) +        else: +            if not hasattr(self._loader, "_files"): +                raise TypeError( +                    "This zip import does not have the required" +                    " metadata to list templates." +                ) + +            # Package is a zip file. +            prefix = ( +                self._template_root[len(self._archive) :].lstrip(os.path.sep) +                + os.path.sep +            ) +            offset = len(prefix) + +            for name in self._loader._files.keys(): +                # Find names under the templates directory that aren't directories. +                if name.startswith(prefix) and name[-1] != os.path.sep: +                    results.append(name[offset:].replace(os.path.sep, "/")) + +        results.sort() +        return results + + +class DictLoader(BaseLoader): +    """Loads a template from a Python dict mapping template names to +    template source.  This loader is useful for unittesting: + +    >>> loader = DictLoader({'index.html': 'source here'}) + +    Because auto reloading is rarely useful this is disabled per default. +    """ + +    def __init__(self, mapping: t.Mapping[str, str]) -> None: +        self.mapping = mapping + +    def get_source( +        self, environment: "Environment", template: str +    ) -> t.Tuple[str, None, t.Callable[[], bool]]: +        if template in self.mapping: +            source = self.mapping[template] +            return source, None, lambda: source == self.mapping.get(template) +        raise TemplateNotFound(template) + +    def list_templates(self) -> t.List[str]: +        return sorted(self.mapping) + + +class FunctionLoader(BaseLoader): +    """A loader that is passed a function which does the loading.  The +    function receives the name of the template and has to return either +    a string with the template source, a tuple in the form ``(source, +    filename, uptodatefunc)`` or `None` if the template does not exist. + +    >>> def load_template(name): +    ...     if name == 'index.html': +    ...         return '...' +    ... +    >>> loader = FunctionLoader(load_template) + +    The `uptodatefunc` is a function that is called if autoreload is enabled +    and has to return `True` if the template is still up to date.  For more +    details have a look at :meth:`BaseLoader.get_source` which has the same +    return value. +    """ + +    def __init__( +        self, +        load_func: t.Callable[ +            [str], +            t.Optional[ +                t.Union[ +                    str, t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]] +                ] +            ], +        ], +    ) -> None: +        self.load_func = load_func + +    def get_source( +        self, environment: "Environment", template: str +    ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]: +        rv = self.load_func(template) + +        if rv is None: +            raise TemplateNotFound(template) + +        if isinstance(rv, str): +            return rv, None, None + +        return rv + + +class PrefixLoader(BaseLoader): +    """A loader that is passed a dict of loaders where each loader is bound +    to a prefix.  The prefix is delimited from the template by a slash per +    default, which can be changed by setting the `delimiter` argument to +    something else:: + +        loader = PrefixLoader({ +            'app1':     PackageLoader('mypackage.app1'), +            'app2':     PackageLoader('mypackage.app2') +        }) + +    By loading ``'app1/index.html'`` the file from the app1 package is loaded, +    by loading ``'app2/index.html'`` the file from the second. +    """ + +    def __init__( +        self, mapping: t.Mapping[str, BaseLoader], delimiter: str = "/" +    ) -> None: +        self.mapping = mapping +        self.delimiter = delimiter + +    def get_loader(self, template: str) -> t.Tuple[BaseLoader, str]: +        try: +            prefix, name = template.split(self.delimiter, 1) +            loader = self.mapping[prefix] +        except (ValueError, KeyError) as e: +            raise TemplateNotFound(template) from e +        return loader, name + +    def get_source( +        self, environment: "Environment", template: str +    ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]: +        loader, name = self.get_loader(template) +        try: +            return loader.get_source(environment, name) +        except TemplateNotFound as e: +            # re-raise the exception with the correct filename here. +            # (the one that includes the prefix) +            raise TemplateNotFound(template) from e + +    @internalcode +    def load( +        self, +        environment: "Environment", +        name: str, +        globals: t.Optional[t.MutableMapping[str, t.Any]] = None, +    ) -> "Template": +        loader, local_name = self.get_loader(name) +        try: +            return loader.load(environment, local_name, globals) +        except TemplateNotFound as e: +            # re-raise the exception with the correct filename here. +            # (the one that includes the prefix) +            raise TemplateNotFound(name) from e + +    def list_templates(self) -> t.List[str]: +        result = [] +        for prefix, loader in self.mapping.items(): +            for template in loader.list_templates(): +                result.append(prefix + self.delimiter + template) +        return result + + +class ChoiceLoader(BaseLoader): +    """This loader works like the `PrefixLoader` just that no prefix is +    specified.  If a template could not be found by one loader the next one +    is tried. + +    >>> loader = ChoiceLoader([ +    ...     FileSystemLoader('/path/to/user/templates'), +    ...     FileSystemLoader('/path/to/system/templates') +    ... ]) + +    This is useful if you want to allow users to override builtin templates +    from a different location. +    """ + +    def __init__(self, loaders: t.Sequence[BaseLoader]) -> None: +        self.loaders = loaders + +    def get_source( +        self, environment: "Environment", template: str +    ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]: +        for loader in self.loaders: +            try: +                return loader.get_source(environment, template) +            except TemplateNotFound: +                pass +        raise TemplateNotFound(template) + +    @internalcode +    def load( +        self, +        environment: "Environment", +        name: str, +        globals: t.Optional[t.MutableMapping[str, t.Any]] = None, +    ) -> "Template": +        for loader in self.loaders: +            try: +                return loader.load(environment, name, globals) +            except TemplateNotFound: +                pass +        raise TemplateNotFound(name) + +    def list_templates(self) -> t.List[str]: +        found = set() +        for loader in self.loaders: +            found.update(loader.list_templates()) +        return sorted(found) + + +class _TemplateModule(ModuleType): +    """Like a normal module but with support for weak references""" + + +class ModuleLoader(BaseLoader): +    """This loader loads templates from precompiled templates. + +    Example usage: + +    >>> loader = ChoiceLoader([ +    ...     ModuleLoader('/path/to/compiled/templates'), +    ...     FileSystemLoader('/path/to/templates') +    ... ]) + +    Templates can be precompiled with :meth:`Environment.compile_templates`. +    """ + +    has_source_access = False + +    def __init__( +        self, path: t.Union[str, os.PathLike, t.Sequence[t.Union[str, os.PathLike]]] +    ) -> None: +        package_name = f"_jinja2_module_templates_{id(self):x}" + +        # create a fake module that looks for the templates in the +        # path given. +        mod = _TemplateModule(package_name) + +        if not isinstance(path, abc.Iterable) or isinstance(path, str): +            path = [path] + +        mod.__path__ = [os.fspath(p) for p in path] + +        sys.modules[package_name] = weakref.proxy( +            mod, lambda x: sys.modules.pop(package_name, None) +        ) + +        # the only strong reference, the sys.modules entry is weak +        # so that the garbage collector can remove it once the +        # loader that created it goes out of business. +        self.module = mod +        self.package_name = package_name + +    @staticmethod +    def get_template_key(name: str) -> str: +        return "tmpl_" + sha1(name.encode("utf-8")).hexdigest() + +    @staticmethod +    def get_module_filename(name: str) -> str: +        return ModuleLoader.get_template_key(name) + ".py" + +    @internalcode +    def load( +        self, +        environment: "Environment", +        name: str, +        globals: t.Optional[t.MutableMapping[str, t.Any]] = None, +    ) -> "Template": +        key = self.get_template_key(name) +        module = f"{self.package_name}.{key}" +        mod = getattr(self.module, module, None) + +        if mod is None: +            try: +                mod = __import__(module, None, None, ["root"]) +            except ImportError as e: +                raise TemplateNotFound(name) from e + +            # remove the entry from sys.modules, we only want the attribute +            # on the module object we have stored on the loader. +            sys.modules.pop(module, None) + +        if globals is None: +            globals = {} + +        return environment.template_class.from_module_dict( +            environment, mod.__dict__, globals +        ) | 
