Skip to content

SOURCE CODE liquid.liquid DOCS

"""Provides Liquid class"""
import builtins
from typing import Any, Callable, Mapping
from jinja2 import (
    Environment,
    ChoiceLoader,
    FileSystemLoader,
)

from .filters.standard import standard_filter_manager
from .utils import PathType, PathTypeOrIter


class Liquid:DOCS
    """The entrance for the package

    Examples:
        >>> Liquid('{{a}}', from_file=False)
        >>> Liquid('template.html')

    Args:
        template: The template string or path of the template file
        env: The jinja environment
        from_file: Whether `template` is a file path. If True, a
            `FileSystemLoader` will be used in the `env`.
        mode: The mode of the engine.
            - standard: Most compatibility with the standard liquid engine
            - jekyll: The jekyll-compatible mode
            - shopify: The shopify-compatible mode
            - wild: The liquid- and jinja-compatible mode
        filter_with_colon: Whether enable to use colon to separate filter and
            its arguments (i.e. `{{a | filter: arg}}`). If False, will
            fallback to use parentheses (`{{a | filter(arg)}}`)
        search_paths: The search paths for the template files.
            This only supports specification of paths. If you need so specify
            `encoding` and/or `followlinks`, you should use jinja's
            `FileSystemLoader`
        globals: Additional global values to be used to render the template
        filters: Additional filters be to used to render the template
        filters_as_globals: Whether also use filters as globals
            Only works in wild mode
        **kwargs: Other arguments for an jinja Environment construction and
            configurations for extensions
    """

    __slots__ = ("env", "template")

    def __init__(
        self,
        template: PathType,
        from_file: bool = None,
        mode: str = None,
        env: Environment = None,
        filter_with_colon: bool = None,
        search_paths: PathTypeOrIter = None,
        globals: Mapping[str, Any] = None,
        filters: Mapping[str, Callable] = None,
        filters_as_globals: bool = None,
        **kwargs: Any,
    ) -> None:
        """Constructor"""
        # default values
        # fetch at runtime, so that they can be configured at importing
        from .defaults import (
            FROM_FILE,
            MODE,
            FILTER_WITH_COLON,
            SEARCH_PATHS,
            ENV_ARGS,
            SHARED_GLOBALS,
            FILTERS_AS_GLOBALS,
        )

        if from_file is None:
            from_file = FROM_FILE
        if mode is None:
            mode = MODE
        if filter_with_colon is None:
            filter_with_colon = FILTER_WITH_COLON
        if search_paths is None:
            search_paths = SEARCH_PATHS
        if filters_as_globals is None:
            filters_as_globals = FILTERS_AS_GLOBALS

        # split kwargs into arguments for Environment constructor and
        # configurations for extensions
        env_args = {}
        ext_conf = {}
        for key, val in kwargs.items():
            if key in ENV_ARGS:
                env_args[key] = val
            else:
                ext_conf[key] = val

        loader = env_args.pop("loader", None)
        fsloader = FileSystemLoader(search_paths)  # type: ignore
        if loader:
            loader = ChoiceLoader([loader, fsloader])
        else:
            loader = fsloader

        self.env = Environment(**env_args, loader=loader)
        if env is not None:
            self.env = env.overlay(**env_args, loader=loader)

        self.env.extend(**ext_conf)
        self.env.globals.update(SHARED_GLOBALS)

        standard_filter_manager.update_to_env(self.env)
        self.env.add_extension("jinja2.ext.loopcontrols")
        if filter_with_colon:
            from .exts.filter_colon import FilterColonExtension

            self.env.add_extension(FilterColonExtension)

        if mode == "wild":
            from .exts.wild import LiquidWildExtension
            from .filters.wild import wild_filter_manager

            self.env.add_extension("jinja2.ext.debug")
            self.env.add_extension(LiquidWildExtension)

            bfilters = {
                key: getattr(builtins, key)
                for key in dir(builtins)
                if not key.startswith("_")
                and callable(getattr(builtins, key))
                and key
                not in (
                    "copyright",
                    "credits",
                    "input",
                    "help",
                    "globals",
                    "license",
                    "locals",
                    "memoryview",
                    "object",
                    "property",
                    "staticmethod",
                    "super",
                )
                and not any(key_c.isupper() for key_c in key)
            }
            self.env.filters.update(bfilters)
            wild_filter_manager.update_to_env(self.env)
            self.env.globals.update(
                {
                    key: val
                    for key, val in __builtins__.items()
                    if not key.startswith("_")
                }
            )
            if filters_as_globals:
                self.env.globals.update(standard_filter_manager.filters)
                self.env.globals.update(wild_filter_manager.filters)

        elif mode == "jekyll":
            from .exts.front_matter import FrontMatterExtension
            from .exts.jekyll import LiquidJekyllExtension
            from .filters.jekyll import jekyll_filter_manager

            jekyll_filter_manager.update_to_env(self.env)
            self.env.add_extension(FrontMatterExtension)
            self.env.add_extension(LiquidJekyllExtension)

        elif mode == "shopify":
            from .exts.shopify import LiquidShopifyExtension
            from .filters.shopify import shopify_filter_manager

            shopify_filter_manager.update_to_env(self.env)
            self.env.add_extension(LiquidShopifyExtension)

        else:  # standard
            from .exts.standard import LiquidStandardExtension

            self.env.add_extension(LiquidStandardExtension)

        if filters:
            self.env.filters.update(filters)

        builtin_globals = {
            "int": int,
            "float": float,
            "str": str,
            "bool": bool
        }
        if globals:
            builtin_globals.update(globals)
        self.env.globals.update(builtin_globals)

        if from_file:
            # in case template is a PathLike
            self.template = self.env.get_template(str(template))
        else:
            self.template = self.env.from_string(str(template))

    def render(self, *args, **kwargs) -> Any:DOCS
        """Render the template.

        You can either pass the values using `tpl.render(a=1)` or
        `tpl.render({'a': 1})`
        """
        return self.template.render(*args, **kwargs)

    async def render_async(self, *args, **kwargs) -> Any:DOCS
        """Asynchronously render the template"""
        return await self.template.render_async(*args, **kwargs)

    @classmethodDOCS
    def from_env(
        cls,
        template: PathType,
        env: Environment,
        from_file: bool = None,
        filter_with_colon: bool = None,
        filters_as_globals: bool = None,
        mode: str = None,
    ) -> "Liquid":
        """Initiate a template from a jinja environment

        You should not specify any liquid-related extensions here. They will
        be added automatically.

        No search path is allow to be passed here. Instead, use jinja2's
        loaders or use the constructor to initialize a template.

        @Args:
            template: The template string or path of the template file
            env: The jinja environment
            from_file: Whether `template` is a file path. If True, a
                `FileSystemLoader` will be used in the `env`.
            filter_with_colon: Whether enable to use colon to separate filter
                and its arguments (i.e. `{{a | filter: arg}}`). If False, will
                fallback to use parentheses (`{{a | filter(arg)}}`)
            filters_as_globals: Whether also use filters as globals
                Only works in wild mode
            mode: The mode of the engine.
                - standard: Most compatibility with the standard liquid engine
                - wild: The liquid- and jinja-compatible mode
                - jekyll: The jekyll-compatible mode

        @Returns:
            A `Liquid` object
        """
        return cls(
            template,
            env=env,
            from_file=from_file,
            filter_with_colon=filter_with_colon,
            filters_as_globals=filters_as_globals,
            mode=mode,
        )