"""
Module containing template specific classes.
"""
import os
import pathlib
import jinja2
from functools import partial, lru_cache
from typing import (
Dict,
List,
Optional,
Union,
Any,
)
from duck.exceptions.all import (
TemplateError,
TemplateNotFound,
SettingsError,
DisallowedAction,
)
from duck.template.templatetags import (
TemplateFilter,
TemplateTag,
)
from duck.utils.path import joinpaths, normalize_url_path
from duck.utils.importer import import_module_once
from duck.settings import SETTINGS
[docs]
@lru_cache(maxsize=1)
def default_jinja2_engine() -> "Jinja2Engine":
"""
Returns the default Jinja2 template engine. This caches the result by default.
"""
return Jinja2Engine.get_default()
[docs]
@lru_cache(maxsize=1)
def default_django_engine() -> "DjangoEngine":
"""
Returns the default Django template engine. This caches the result by default.
"""
return DjangoEngine.get_default()
[docs]
class Template:
"""
Template class for all base templates.
"""
__slots__ = {
"context",
"name",
"origin",
"engine",
}
def __init__(
self,
context: Optional[Dict] = None,
name: Optional[str] = None,
origin: Optional[str] = None,
engine: "Engine" = None,
):
"""
Initialize template class.
Args:
context (dict, optional): The template context
name (str, optional): The name of the template. Name will be resolved from origin.
origin (str, optional): The asolute template path.
engine (Engine): The template engine.
"""
self.context = context or {}
self.origin = origin
self.name = name or pathlib.Path(origin).basename() if origin else name
self.engine = engine or Engine.get_default()
[docs]
def render_template(self) -> str:
return self.engine.render_template(self)
[docs]
class Engine:
"""
Engine class for templates.
Notes:
For all engines, Duck internal and custom template tags and filters are enabled by default.
"""
[docs]
@classmethod
@lru_cache(maxsize=1)
def get_default(cls):
"""
Returns the default template engine, i.e. DjangoEngine.
"""
return DjangoEngine()
[docs]
def get_template(self, template_name: str) -> str:
"""
Custom method for retrieving template data accordingly.
"""
env = None # The real environment e.g. Jinja2 or Django.
# Normalize template path to make it easy for getting path parts
template_name = normalize_url_path(template_name)
if isinstance(self, Jinja2Engine):
env = self.environment
elif isinstance(self, DjangoEngine):
env = self._django_engine
else:
raise TemplateError("Unknown engine, expected DjangoEngine or Jinja2Engine for self.")
# First lookup for template in global dirs.
return env.get_template(template_name)
[docs]
def render_template(self, template: Template):
"""
Returns rendered content.
"""
raise NotImplementedError("Implement method `render_template`.")
[docs]
class Jinja2Engine(Engine):
"""
Jinja2 engine class.
"""
__slots__ = {
"_duck_templatetags",
"custom_templatetags",
"environment",
"_jinja2__environment",
"loader",
}
def __init__(
self,
autoescape: bool = True,
custom_templatetags: Optional[List[Union[TemplateTag, TemplateFilter]]] = None,
environment: Optional[jinja2.Environment] = None,
loader: Any = None,
):
from duck.settings.loaded import SettingsLoaded
from duck.template.loaders import Jinja2FileSystemLoader, BaseLoader # Best for Duck use-case
self._duck_templatetags = SettingsLoaded.ALL_TEMPLATETAGS
self.autoescape = autoescape
self.custom_templatetags = custom_templatetags or []
self.loader = loader or Jinja2FileSystemLoader()
self.environment = environment or self.get_default_environment()
if not isinstance(self.loader, BaseLoader):
raise TypeError(f"Loader must be a subbclass of BaseLoader not {type(loader)}")
# Setup jinja2 environment.
self.setup_environment()
[docs]
def get_default_environment(self) -> jinja2.Environment:
"""
Returns the appropriate jinja2 environment.
"""
if not hasattr(self, "_jinja2__environment"):
self._jinja2__environment = jinja2.Environment(loader=self.loader)
return self._jinja2__environment
[docs]
def setup_environment(self):
"""
Setups the jinja2 environment.
"""
templatetags = self._duck_templatetags + self.custom_templatetags
self.environment.autoescape = self.autoescape
# Apply tags and filters available in Duck project.
self.apply_templatetags(templatetags)
[docs]
def render_template(self, template: Template) -> str:
"""
This renders a template into static content.
Args:
template (Template): The template object.
Returns:
rendered_template (str): Rendered template as string
"""
if not isinstance(template, Template):
raise TemplateError(f"The template object must be an instance of Template not {type(template)}")
try:
jinja2_template = self.get_template(template.name)
return jinja2_template.render(template.context)
except jinja2.exceptions.TemplateNotFound as e:
raise TemplateNotFound(f"If using a blueprints, ensure that `blueprint.enable_template_dir=True`: {e}")
except Exception as e:
raise TemplateError(f"Error rendering template `{template.origin or template.name or template}`: {e}")
[docs]
class DjangoEngine(Engine):
"""
Django engine class.
"""
__slots__ = {
"_duck_templatetags",
"autoescape",
"libraries",
"_django_engine",
"_imported_django_settings_module",
"_django__engine",
"loaders",
}
def __init__(
self,
autoescape: bool = True,
libraries: Optional[List[str]] = None,
_django_engine: Optional[Any] = None,
loaders: List[str] = None,
):
self._duck_templatetags: List[str] = ["duck.backend.django.templatetags.ducktags"] # all internal and custom template tags and filters
self.autoescape = autoescape
self.libraries = libraries
self._django_engine = _django_engine or self.get_default_django_engine()
self.loaders = loaders or ["duck.template.loaders.DjangoFileSystemLoader"]
self.setup_django_engine()
[docs]
@classmethod
@lru_cache(maxsize=1)
def get_default(cls):
"""
Returns the default Django engine.
"""
return DjangoEngine()
[docs]
def get_default_django_engine(self):
"""
Returns the django default template engine.
"""
from django.template import Engine as _Engine
from django.core.exceptions import ImproperlyConfigured
# Attempt to import the local Django project settings
try:
if not hasattr(self, '_imported_django_settings_module'):
import_module_once(SETTINGS['DJANGO_SETTINGS_MODULE'])
self._imported_django_settings_module = True
except (ImportError, KeyError, ModuleNotFoundError):
raise SettingsError("Please make sure that the Django project structure for Duck is correct for you to use Django template engine.")
if not hasattr(self, "_django__engine"):
try:
self._django__engine = _Engine.get_default()
except ImproperlyConfigured:
raise SettingsError(
"Please make sure that the Django project structure for Duck is correct for you to use Django template engine. "
"Also ensure that DJANGO_SETTINGS_MODULE is set correctly in Duck settings.py."
)
return self._django__engine
[docs]
def setup_django_engine(self):
"""
Setups the inner django engine.
"""
self._django_engine.autoescape = self.autoescape
self._django_engine.loaders.extend(self.loaders)
# Apply template tags and filters.
self.apply_templatetags(
builtin_libraries=self._duck_templatetags,
custom_libraries=self.libraries,
)
[docs]
def render_template(self, template: Template) -> str:
"""
Returns rendered content.
"""
from django.template import Context, TemplateDoesNotExist
if not isinstance(template, Template):
raise TemplateError(f"The template object must be an instance of Template not {type(template)}")
try:
django_template = self.get_template(template.name)
return django_template.render(Context(template.context))
except TemplateDoesNotExist as e:
raise TemplateNotFound(f"Template not found. If using a blueprints, ensure that `blueprint.enable_template_dir=True`: {e}")
except Exception as e:
raise TemplateError(f"Error rendering template `{template.origin or template.name or template}`: {e}")