Source code for litestar.plugins.minijinja

"""MiniJinja template engine integration for Litestar."""

import functools
from pathlib import Path
from typing import TYPE_CHECKING, Any, Protocol, TypeVar

from typing_extensions import ParamSpec

from litestar.exceptions import ImproperlyConfiguredException, MissingDependencyException, TemplateNotFoundException
from litestar.template.base import (
    TemplateCallableType,
    TemplateEngineProtocol,
    TemplateProtocol,
    csrf_token,
    url_for,
)

try:
    from minijinja import Environment
    from minijinja import TemplateError as MiniJinjaTemplateNotFound
except ImportError as e:
    raise MissingDependencyException("minijinja") from e

if TYPE_CHECKING:
    from collections.abc import Callable, Mapping

    C = TypeVar("C", bound="Callable")

    def pass_state(func: C) -> C: ...

else:
    from minijinja import pass_state

__all__ = (
    "MiniJinjaTemplateEngine",
    "StateProtocol",
)

P = ParamSpec("P")
T = TypeVar("T")


[docs] class StateProtocol(Protocol): auto_escape: "str | None" current_block: "str | None" env: Environment name: str def lookup(self, key: str) -> "Any | None": ...
def _transform_state( func: TemplateCallableType["Mapping[str, Any]", P, T], ) -> TemplateCallableType[StateProtocol, P, T]: """Transform a template callable to receive a ``StateProtocol`` instance as first argument. This is for wrapping callables like ``url_for()`` that receive a mapping as first argument so they can be used with minijinja which passes a ``StateProtocol`` instance as first argument. """ @functools.wraps(func) @pass_state def wrapped(state: StateProtocol, /, *args: P.args, **kwargs: P.kwargs) -> T: template_context = {"request": state.lookup("request"), "csrf_input": state.lookup("csrf_input")} return func(template_context, *args, **kwargs) return wrapped class MiniJinjaTemplate(TemplateProtocol): """Initialize a template. Args: template: Base ``MiniJinjaTemplate`` used by the underlying minijinja engine """ def __init__(self, engine: Environment, template_name: str) -> None: super().__init__() self.engine = engine self.template_name = template_name def render(self, *args: Any, **kwargs: Any) -> str: """Render a template. Args: args: Positional arguments passed to the engines ``render`` function kwargs: Keyword arguments passed to the engines ``render`` function Returns: Rendered template as a string """ try: return str(self.engine.render_template(self.template_name, *args, **kwargs)) except MiniJinjaTemplateNotFound as err: raise TemplateNotFoundException(template_name=self.template_name) from err
[docs] class MiniJinjaTemplateEngine(TemplateEngineProtocol["MiniJinjaTemplate", StateProtocol]): """The engine instance."""
[docs] def __init__( self, directory: "Path | list[Path] | None" = None, engine_instance: "Environment | None" = None, ) -> None: """Minijinja based TemplateEngine. Args: directory: Direct path or list of directory paths from which to serve templates. engine_instance: A Minijinja Environment instance. """ if directory and engine_instance: raise ImproperlyConfiguredException( "You must provide either a directory or a minijinja Environment instance." ) if directory: def _loader(name: str) -> str: """Load a template from a directory. Args: name: The name of the template Returns: The template as a string Raises: TemplateNotFoundException: if no template is found. """ directories = directory if isinstance(directory, list) else [directory] for d in directories: template_path = Path(d) / name if template_path.exists(): return template_path.read_text() raise TemplateNotFoundException(template_name=name) self.engine = Environment(loader=_loader) elif engine_instance: self.engine = engine_instance else: raise ImproperlyConfiguredException( "You must provide either a directory or a minijinja Environment instance." ) self.register_template_callable("url_for", _transform_state(url_for)) self.register_template_callable("csrf_token", _transform_state(csrf_token))
[docs] def get_template(self, template_name: str) -> MiniJinjaTemplate: """Retrieve a template by matching its name (dotted path) with files in the directory or directories provided. Args: template_name: A dotted path Returns: MiniJinjaTemplate instance Raises: TemplateNotFoundException: if no template is found. """ return MiniJinjaTemplate(self.engine, template_name)
[docs] def register_template_callable( self, key: str, template_callable: TemplateCallableType[StateProtocol, P, T], ) -> None: """Register a callable on the template engine. Args: key: The callable key, i.e. the value to use inside the template to call the callable. template_callable: A callable to register. Returns: None """ def is_decorated(func: "Callable") -> bool: return hasattr(func, "__wrapped__") or func.__name__ not in globals() if not is_decorated(template_callable): template_callable = _transform_state(template_callable) # type: ignore[arg-type] # pragma: no cover self.engine.add_global(key, pass_state(template_callable))
[docs] def render_string(self, template_string: str, context: "Mapping[str, Any]") -> str: """Render a template from a string with the given context. Args: template_string: The template string to render. context: A dictionary of variables to pass to the template. Returns: The rendered template as a string. """ return self.engine.render_str(template_string, **context)
[docs] @classmethod def from_environment(cls, minijinja_environment: "Environment") -> "MiniJinjaTemplateEngine": """Create a MiniJinjaTemplateEngine from an existing minijinja Environment instance. Args: minijinja_environment (Environment): A minijinja Environment instance. Returns: MiniJinjaTemplateEngine instance """ return cls(directory=None, engine_instance=minijinja_environment)