Template Engine Backends
The template engine abstraction layer provides a unified interface for interacting with different template systems. By decoupling the high-level template loading and rendering logic from the specific syntax and implementation of an engine, this project allows developers to use the Django Template Language (DTL), Jinja2, or custom backends interchangeably.
The Engine Handler
The entry point for managing template engines is the EngineHandler class, located in django/template/utils.py. This class is responsible for parsing the TEMPLATES setting and instantiating the configured backends.
In practice, the framework uses a global instance of EngineHandler accessible via django.template.engines. This handler caches engine instances to ensure they are only initialized once.
# django/template/utils.py
class EngineHandler:
def __init__(self, templates=None):
self._templates = templates
self._engines = {}
def __getitem__(self, alias):
try:
return self._engines[alias]
except KeyError:
# ... logic to find config in settings.TEMPLATES ...
params = params.copy()
backend = params.pop("BACKEND")
engine_cls = import_string(backend)
engine = engine_cls(params)
self._engines[alias] = engine
return engine
When you access engines['django'], the handler looks up the configuration for the alias "django", imports the backend class specified in the BACKEND key, and initializes it with the provided parameters.
The Base Engine Interface
All template backends must inherit from BaseEngine in django/template/backends/base.py. This class defines the contract that every engine must fulfill and provides shared logic for template discovery.
Core Methods
Subclasses are required to implement the following methods:
get_template(template_name): Loads a template by name and returns a backend-specific template object.from_string(template_code): Creates a template object from a string of source code.
Template Discovery
BaseEngine implements a standardized way to locate template files using the DIRS and APP_DIRS settings. The template_dirs property aggregates directories from the explicit DIRS list and, if APP_DIRS is true, from the app_dirname of installed applications.
# django/template/backends/base.py
@cached_property
def template_dirs(self):
template_dirs = tuple(self.dirs)
if self.app_dirs:
template_dirs += get_app_template_dirs(self.app_dirname)
return template_dirs
The iter_template_filenames method uses these directories to generate candidate paths for a given template name, ensuring that the resulting paths are safe from directory traversal attacks via safe_join.
Django Template Language Backend
The DjangoTemplates backend in django/template/backends/django.py is the default implementation for the Django Template Language. It acts as a wrapper around the internal django.template.engine.Engine.
Key characteristics of this backend include:
- App Directory: It sets
app_dirname = "templates", meaning it looks for templates in atemplates/subdirectory within each installed app. - Library Management: It automatically discovers template tag libraries from installed apps via
get_installed_libraries(). - Validation: It implements a
check()method to detect configuration errors, such as non-string values forstring_if_invalidor duplicate template tag library names.
# django/template/backends/django.py
class DjangoTemplates(BaseEngine):
app_dirname = "templates"
def __init__(self, params):
params = params.copy()
options = params.pop("OPTIONS").copy()
# ... setup default options ...
super().__init__(params)
self.engine = Engine(self.dirs, self.app_dirs, **options)
def get_template(self, template_name):
try:
return Template(self.engine.get_template(template_name), self)
except TemplateDoesNotExist as exc:
reraise(exc, self)
Jinja2 Backend
The Jinja2 backend in django/template/backends/jinja2.py provides integration for the Jinja2 template engine.
Unlike the DTL backend, the Jinja2 backend:
- App Directory: Sets
app_dirname = "jinja2". - Environment Initialization: It initializes a
jinja2.Environment(or a custom subclass specified inOPTIONS['environment']). - Context Processors: It supports Django-style context processors, which are imported and stored in
template_context_processors.
# django/template/backends/jinja2.py
class Jinja2(BaseEngine):
app_dirname = "jinja2"
def __init__(self, params):
# ...
self.context_processors = options.pop("context_processors", [])
environment = options.pop("environment", "jinja2.Environment")
environment_cls = import_string(environment)
if "loader" not in options:
options["loader"] = jinja2.FileSystemLoader(self.template_dirs)
self.env = environment_cls(**options)
Direct Backend Usage
While most template interaction happens through django.template.loader.get_template, some components instantiate backends directly to create isolated environments. A primary example is found in django/forms/renderers.py, where form renderers use specific backends to load widget templates independently of the global TEMPLATES setting.
# django/forms/renderers.py
class EngineMixin:
@cached_property
def engine(self):
return self.backend(
{
"APP_DIRS": True,
"DIRS": [Path(__file__).parent / self.backend.app_dirname],
"NAME": "djangoforms",
"OPTIONS": {},
}
)
class DjangoTemplates(EngineMixin, BaseRenderer):
backend = DjangoTemplates
In this pattern, the backend class is called with a configuration dictionary identical to what would be found in settings.TEMPLATES, allowing the renderer to control exactly where its templates are loaded from.