from __future__ import annotations
from collections.abc import Mapping
from functools import partial
from typing import TYPE_CHECKING, Any, 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 mako.exceptions import TemplateLookupException as MakoTemplateNotFound # type: ignore[import-untyped]
from mako.lookup import TemplateLookup # type: ignore[import-untyped]
from mako.template import Template as _MakoTemplate # type: ignore[import-untyped]
except ImportError as e:
raise MissingDependencyException("mako") from e
if TYPE_CHECKING:
from pathlib import Path
__all__ = ("MakoTemplate", "MakoTemplateEngine")
P = ParamSpec("P")
T = TypeVar("T")
[docs]
class MakoTemplate(TemplateProtocol):
"""Mako template, implementing ``TemplateProtocol``"""
[docs]
def __init__(self, template: _MakoTemplate, template_callables: list[tuple[str, TemplateCallableType]]) -> None:
"""Initialize a template.
Args:
template: Base ``MakoTemplate`` used by the underlying mako-engine
template_callables: List of callables passed to the template
"""
super().__init__()
self.template = template
self.template_callables = template_callables
[docs]
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
"""
for callable_key, template_callable in self.template_callables:
kwargs_copy = {**kwargs}
kwargs[callable_key] = partial(template_callable, kwargs_copy)
return str(self.template.render(*args, **kwargs))
[docs]
class MakoTemplateEngine(TemplateEngineProtocol[MakoTemplate, Mapping[str, Any]]):
"""Mako-based TemplateEngine."""
[docs]
def __init__(self, directory: Path | list[Path] | None = None, engine_instance: Any | None = None) -> None:
"""Initialize template engine.
Args:
directory: Direct path or list of directory paths from which to serve templates.
engine_instance: A mako TemplateLookup instance.
"""
if directory and engine_instance:
raise ImproperlyConfiguredException("You must provide either a directory or a mako TemplateLookup.")
if directory:
self.engine = TemplateLookup(
directories=directory if isinstance(directory, (list, tuple)) else [directory], default_filters=["h"]
)
elif engine_instance:
self.engine = engine_instance
self._template_callables: list[tuple[str, TemplateCallableType]] = []
self.register_template_callable(key="csrf_token", template_callable=csrf_token)
self.register_template_callable(key="url_for", template_callable=url_for)
[docs]
def get_template(self, template_name: str) -> MakoTemplate:
"""Retrieve a template by matching its name (dotted path) with files in the directory or directories provided.
Args:
template_name: A dotted path
Returns:
MakoTemplate instance
Raises:
TemplateNotFoundException: if no template is found.
"""
try:
return MakoTemplate(
template=self.engine.get_template(template_name), template_callables=self._template_callables
)
except MakoTemplateNotFound as exc:
raise TemplateNotFoundException(template_name=template_name) from exc
[docs]
def register_template_callable(
self, key: str, template_callable: TemplateCallableType[Mapping[str, Any], 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
"""
self._template_callables.append((key, template_callable))
[docs]
def render_string(self, template_string: str, context: Mapping[str, Any]) -> str: # pyright: ignore
"""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.
"""
template = _MakoTemplate(template_string) # noqa: S702
return template.render(**context) # type: ignore[no-any-return]
[docs]
@classmethod
def from_template_lookup(cls, template_lookup: TemplateLookup) -> MakoTemplateEngine:
"""Create a template engine from an existing mako TemplateLookup instance.
Args:
template_lookup: A mako TemplateLookup instance.
Returns:
MakoTemplateEngine instance
"""
return cls(directory=None, engine_instance=template_lookup)