Multiple Template Engines > Updates - Funders - DEP - Campaign
DEP: | 182 |
---|---|
Type: | Feature |
Status: | Final |
Created: | 2014-09-14 |
Last-Modified: | 2015-10-02 |
Author: | Aymeric Augustin |
Implementation-Team: | |
Aymeric Augustin | |
Shepherd: | Carl Meyer |
Django-Version: | 1.8 |
Resolution: | Accepted |
Table of Contents
Support some alternate template engines such as Jinja2 out of the box.
Keep the Django Template Language as the default template engine.
Provide a stable API for integrating third-party template engines.
Support multiple template engines within the same Django project.
The Django Template Language (DTL) is quite opinionated. It is purposefully designed to limit the amount of logic that can be embedded in templates. This choice keeps business logic outside of templates. Sometimes it also pushes display logic into views.
Custom logic can be expressed through custom template filters or tags. APIs such as simple_tag, inclusion_tag and assignment_tag make common use cases easier. Still, writing custom template tags can be hard. Often it results in messy code.
Furthermore the DTL can be slow to render complex templates. While this isn't an issue for many simple websites, complex pages may suffer from the cost of interpreting templates in Python. Poor performance has blocked efforts to introduce template-based widget rendering, leaving Django forms stuck with concatenating hardcoded pieces of HTML in Python.
PyPy improves rendering speed a lot. However, in 2014, PyPy isn't ready for being recommended as Django's default deployment platform. Its support for Python 3 is still experimental. PyPy is still a second-class citizen of the Python ecosystem. For instance, well-known Linux distributions don't ship a WSGI server running on PyPy out of the box.
Finally, attempts to optimize rendering performance have failed.
For at least these two reasons, convenience and performance, Django users are increasingly turning to alternate template engines. Jinja2 is the most popular choice thanks to its syntax inspired by the DTL and its excellent performance.
Given Django's loose coupling philosophy, it is relatively easy to swap the template engine. However seamless integration requires a non-trivial amount of code. For example half a dozen libraries compete for providing integration between Django and Jinja2.
Therefore, this DEP proposes:
The operation of a template engine can be split in three steps:
When this document discusses configuring, loading or rendering, it refers to these steps or to their implementation.
The Django Template Language hasn't evolved much over the years. It carries several design decisions made in 2005. Nine years later, if the Django team started from a clean slate, it would make different decisions.
Therefore this project avoids encoding the legacy of the DTL in APIs. It doesn't encourage third-party engines to provide compatibility with the DTL. Instead it focuses on integration with other components of Django.
Maintainers of third-party engines are welcome to make almost any design decision they want. The main exception is security. This DEP is prescriptive when it comes to security considerations:
Supporting pluggable engines is a strategy that has served Django well in many areas. It's more valuable in the long term than just merging a mature Django - Jinja2 adapter.
The Django Template Language must remain the default to avoid creating a huge backwards incompatibility without an acceptable upgrade path for the ecosystem at large.
Support for template strings is built-in to validate a minimal implementation. This is akin to the local memory cache backend or, to a lesser extent, to the SQLite database backend.
Support for Jinja2 is built-in because it appears to be the most widely used alternative. No one asked for built-in support for another engine when this DEP was discussed.
Support for other template engines is expected to be provided by third-party libraries. The reasons for doing so are exactly the same as for the cache and database engines.
Developers must be able to select the most appropriate engine for each page e.g. use Jinja2 only for a few performance-intensive pages. Also this provides a better migration story for converting a website from one engine to another. That's why Django must support several template engines within the same project.
If several template engines are configured, when tasked with rendering a given template, Django must choose one. There are at least four ways to do this:
Explicitly selecting an engine, for example:
html = render_to_string('index.html', context, using='jinja2')
Not only does this add some inconvenient boilerplate, regardless of the API that's chosen, but worse, each view requires a particular template engine. A developer integrating a third-party application finds themselves unable to replace built-in templates with templates written for another engine.
Explicitly tagging templates, for example:
{# language: jinja2 #}
This works like charset declaration in Python modules. Unfortunately, due to the way template engines are implemented, Django would have to locate the template, figure out which engine it uses, and then the engine would locate the template again, load it and render it. That would restrict engines to selection mechanisms that Django implements and introduce an unhealthy amount of duplication as well as a risk of inconsistencies.
Convention: the file extension would define which engine to use. That's a pragmatic solution. Ruby on Rails would likely take this route.
However, since the Django ecosystem favors configuration over convention, most Django - Jinja2 bridges provide a setting that controls which templates must be rendered with Jinja2. That setting defines a regular expression against which template names are tested.
If extensions are configurable, there's a risk that pluggable apps will end up with incompatible requirements. For example, if app A wants .html files to be rendered with the DTL and app B wants them to be rendered with Jinja2, it becomes impossible to use both apps in the same project. A configuration mechanism that handles such cases would be too complex.
If extensions are enforced, some users will be have to use file names that they don't like or that their editors don't handle well. The potential for bikeshedding makes this an unattractive option. Finally template loaders that don't store templates in the filesystem may use identifiers without a file extension.
Trial and error: in order to load a template, Django would iterate over the list of configured template engines and attempt to locate the template with each of them until one succeeds.
Since there's no way to ascertain whether a particular file is intended for a given template engine, engines that load templates from the filesystem should search for templates in distinct locations. Each engine must have its own list of directories to load templates from and these lists mustn't overlap.
As a consequence, a convention would still be necessary to give each engine its own subdirectory within installed applications to load templates from. This should simply be the engine's name e.g. /jinja2/ for Jinja2. In order to preserve backwards-compatibility, it would remain /templates/ for the DTL. This convention has a lower impact on users because editors don't care about directory names the same way they do about file extensions.
In a project that is developed so that only one engine will find a template with a given identifier, the order of template engines doesn't matter. However it's also possible to rely on this order to implement fallback schemes. For instance, if a pluggable application uses the DTL, a developer can provide Jinja2 replacements for its templates by putting Jinja2 before the DTL in the TEMPLATES setting described below.
Option 4 appears to provide the best compromise. It isn't perfect but it beats the alternatives and it doesn't have any drawbacks for daily use. It creates a healthy separation between templates designed for each engine.
In addition, option 1 will be provided because it gives developers low-level control for atypical use cases. They can implement their own scheme if option 4 doesn't work for them. It won't add much complexity to the implementation.
Template engines are configured in a new setting called TEMPLATES. Here's an example showcasing all possibilities:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True, }, { 'BACKEND': 'django.template.backends.jinja2.Jinja2', 'DIRS': [os.path.join(BASE_DIR, 'jinja2')], 'OPTIONS': { 'extensions': ['jinja2.ext.loopcontrols'], }, }, { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'NAME': 'fallback', 'DIRS': [os.path.join(BASE_DIR, 'fallback_templates')], }, ]
The structure bears some similarity with DATABASES and CACHES but it's a list rather than a dict because the order matters in some cases.
BACKEND is a dotted Python path to a template engine class implementing Django's template backend API as specified below.
NAME must be unique across configured template engines. It's an identifier that allows selecting an engine for rendering. It defaults to the name of the module defining the engine class i.e. the penultimate piece of BACKEND.
Since most engines load templates from files, the top-level configuration for each engine contains two normalized settings:
APP_DIRS is a boolean rather than the name of the subdirectory because that name is a property of the template engine, not a property of the project. It must be shared by all applications for interoperability of pluggable apps.
Engine-specific settings go inside an OPTIONS dictionary which defaults to {}. The intent is that they will be passed as keyword arguments when initializing the template engine.
Loading and rendering look like they could be handled independently, but they're coupled as soon as a template extends or includes another one, as the renderer needs to call the loader. Thus Django must have each template engine configure and use its own loading infrastructure.
With its default settings, Django loads templates from directories listed in the TEMPLATE_DIRS setting and from the 'templates' subdirectories inside installed applications. The latter allows pluggable applications to ship templates.
These basic features should be provided by all template engines according to the values of DIRS and APP_DIRS. Each engine should define a conventional name for the subdirectory contaning its templates inside an installed application. Django searches templates first in directories listed in DIRS and then in installed applications if APP_DIRS is True.
If an engine can't support these features, it must raise an exception when it's configured with a non-empty DIRS or with an APP_DIRS set to True.
At their discretion, engines may provide:
Such engine-specific features are configured in OPTIONS.
Template engines must provide automatic HTML escaping to protect against XSS attacks. It must be enabled by default for two reasons:
Autoescaping is disabled by default in Jinja2, leaving it up the developer to define which variables need escaping and favoring performance over security. The Django adapter will reverse this default.
If an object provides an __html__ method, template engines should assume that it can be used to get a safe HTML representation of the object. The result is guaranteed to be convertible into a str on Python 3 and a unicode on Python 2 but it may be a subclass. This convention provides interoperability between django.utils.safestring and template engines.
Furthermore, when a template is rendered with a reference to the current request, for instance by using the render shortcut, template engines must make the CSRF token available in the context, ideally with an equivalent of Django's {% csrf_token %} tag.
This makes it less likely that developers encounter problems with the CSRF protection framework and choose to simply disable it.
There are two sides to internationalizing templates:
The former isn't an issue. Each template engine can provide a wrapper for the functions from django.utils.translation or recommend an idiomatic way to invoke them.
The latter is more involved because the current implementation of the makemessages management command is inflexible in three ways — see the appendix for details:
Perhaps each template engine could provide a list of template files it can handle and implement a suitable extraction process for translatable strings. However this raises several questions.
Given this complexity, improvements to the internationalization APIs are considered out of scope of this DEP. If it appears useful to formalize a better API, another DEP can be written on that topic.
Until then Jinja2 users will use Babel to extract translatable strings.
The startapp and startproject management commands won't support alternative template engines for now. While it would be feasible to add a --backend/-b option, it would only support built-in backends, because these commands run without configured settings. That makes the feature less attractive.
The entry point for a template engine is the class designated by the 'BACKEND' entry in its configuration.
This class must inherit django.template.backends.base.BaseEngine or implement the following interface.
from django.core.exceptions import ImproperlyConfigured class BaseEngine(object): # Core methods: engines have to provide their own implementation # (except for from_string which is optional). def __init__(self, params): """ Initializes the template engine. Receives the configuration settings as a dict. """ params = params.copy() self.name = params.pop('NAME') self.dirs = list(params.pop('DIRS')) self.app_dirs = bool(params.pop('APP_DIRS')) if params: raise ImproperlyConfigured( "Unknown parameters: {}".format(", ".join(params))) @property def app_dirname(self): raise ImproperlyConfigured( "{} doesn't support loading templates from installed " "applications.".format(self.__class__.__name__)) def from_string(self, template_code): """ Creates and returns a template for the given source code. This method is optional. """ raise NotImplementedError( "subclasses of BaseEngine should provide " "a from_string() method") def get_template(self, template_name): """ Loads and returns a template for the given name. Raises TemplateDoesNotExist if no such template exists. """ raise NotImplementedError( "subclasses of BaseEngine must provide " "a get_template() method")
BaseEngine will also provide utilities that most backends will need.
Template objects returned by backends must conform to the following interface. Django won't provide a BaseTemplate class because it would have only one abstract method.
from django.template.backends.utils import csrf_input_lazy, csrf_token_lazy class BaseTemplate(object): def render(self, context=None, request=None): """ Render this template with a given context. If ``context`` is provided, it must be a ``dict``. If ``request`` is provided, it must be a ``django.http.HttpRequest``. """ if context is None: context = {} if request is not None: # Passing the CSRF token is mandatory. Helpers are available. context['csrf_input'] = csrf_input_lazy(request) context['csrf_token'] = csrf_token_lazy(request) # Passing the request is optional. As Django doesn't have a # global request object, it's useful to put it in the context. context['request'] = request raise NotImplementedError( "subclasses of BaseTemplate must provide a render() method")
Engine and Template classes in adapters should wrap corresponding classes from the underlying libraries rather than inherit them in order to minimize the risk of name clashes.
Template backends must be thread-safe.
The Django Template Language will be refactored into a standalone library.
It will encapsulate its runtime configuration into an instance of a DjangoTemplates class.
Context processors will be moved from django.core.context_processors to django.template.context_processors with a deprecation period. Since users will have to write a new TEMPLATES setting, it's a good time to clean up this historical anomaly.
Here's the default configuration for the Django backend:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'NAME': 'django', 'DIRS': [], 'APP_DIRS': False, 'OPTIONS': { 'allowed_include_roots': [], 'context_processors': [], 'debug': settings.DEBUG, 'loaders': None, 'string_if_invalid': '', file_charset=settings.FILE_CHARSET, }, }, ]
When the 'LOADERS' option isn't set, Django configures:
When the 'LOADERS' option is set, Django:
If TEMPLATES isn't defined at all, for the duration of a deprecation period, Django will automatically build a backwards compatible version as follows:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': settings.TEMPLATE_DIRS, 'OPTIONS': { 'allowed_include_roots': settings.ALLOWED_INCLUDE_ROOTS, 'context_processors': settings.TEMPLATE_CONTEXT_PROCESSORS, 'debug': settings.TEMPLATE_DEBUG, 'loaders': settings.TEMPLATE_LOADERS, 'string_if_invalid': settings.TEMPLATE_STRING_IF_INVALID, }, }, ]
Jinja2 will become an optional dependency of Django.
Here's the default configuration for the Jinja2 backend:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.jinja2.Jinja2', 'NAME': 'jinja2' 'DIRS': [], 'APP_DIRS': False, 'OPTIONS': { 'environment': 'jinja2.Environment', }, }, ]
The main option is 'environment'. It's a dotted Python path to a callable returning a Jinja2 environment. It defaults to 'jinja2.Environment'. Django invokes that callable and passes other options as keyword arguments. Furthermore, Django uses defaults that differ from Jinja2's for a few options if they aren't set explicitly:
Here's an example that uses the default settings and adds a few utilities to the global namespace:
# <project_name>/jinja2.py # Django should provide a public API for this purpose. from django.contrib.staticfiles.storage import staticfiles_storage from django.core.urlresolvers import reverse from jinja2 import Environment def environment(**options): env = Environment(**options) env.globals.update({ 'reverse': reverse, 'static': staticfiles_storage.url, }) return env
The 'environment' option would be set to <project_name>.jinja2.environment.
This backend is built on top of Template strings. It's a proof of concept.
It doesn't accept any options. Its configuration looks as follows:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.dummy.TemplateStrings', 'NAME': 'dummy', 'DIRS': [], 'APP_DIRS': False, }, ]
The current public APIs are:
The new public APIs are:
dictionary is renamed to context because it's a better name and because it's consistent with template responses. This is transparent when it's passed as a positional argument, which is the most common idiom. A deprecation path is provided for when it's passed as a keyword argument.
context_instance is deprecated in favor of context. A compatibility shim will allow passing a Context or a RequestContext in context during the deprecation period.
using provides a way to select a template engine explicitly.
render_to_response gains a status argument for consistency with render which gained it in 0fef92f6.
current_app is used by the {% url %} tag for reversing namespaced URLs. Such coupling is embarrassing. It doesn't serve any other purpose. There are two alternatives to hardcoding this feature in the template rendering API: looking up current_app as an attribute of request or as a value in context. The former makes more sense because the current application is really a property of the request being handled and because current_app is only supported by RequestContext. For these reasons the current_app keyword argument of render is deprecated in favor of a current_app attribute of request.
dirs is new in Django 1.7 and deprecated without a replacement in Django 1.8. Only the Django Template Language will support it in Django 1.8 and 1.9. It was added in 2f0566fa in order to fix ticket #4278. Unfortunately that ticket was very old and no longer made sense once template loaders were introduced. Besides the current implementation doesn't even work: dirs doesn't apply to extended or included templates.
The current public APIs are:
current_app is treated exactly like for render.
All backwards-incompatible changes to public APIs will go through a deprecation path according to Django's API stability policy. Notable changes include:
Since this project involves a large amount of refactoring, many private APIs will change. In order to clarify the landscape, private APIs imported in the django.template namespace will be removed. Only public APIs will be left. The author will make an effort to provide a deprecation path or document the removal of private APIs that are likely to be used in the wild.
Django's documentation describes the Django Template Language in four pages:
The syntax of the Django Template Language supports four constructs:
In addition, its rendering engine provides four notable features:
It also provides debatable "designer-friendly" error handling.
Currently Django provides six settings to configure its template engine:
Except for TEMPLATE_DEBUG, all these settings should become options in the configuration of Django template backends and lose their TEMPLATE_ prefix.
The template engine also takes a few other settings into account:
Django ships four loaders, two of which are enabled by default:
Loaders are invoked through global APIs: get_template and select_template.
Custom loaders are implemented by subclassing BaseLoader and overriding load_template_source.
The documentation describes how to return a non-DTL template from a loader. While this is a reasonable point to interface with a third-party template engine, the current API requires lots of glue code. That's why this proposal offers a more structured solution.
In addition to the expected Template class, there are two Context classes:
In order to preserve loose coupling, Context doesn't know anything about HTTP requests. But almost all templates need values from the request. RequestContext is the pragmatic answer: it's instantiated with request and passes it to context processors.
Built-in context processors are defined in django.core.context_processors. They were introduced in 49fd163a and b28e5e41. At that time, the template engine was implemented in django.core.template. The magic-removal refactor moved the template engine to django.template but didn't touch context processors.
Context processors make various bits of Django easier to interact with in templates. They don't quite belong to django.core. In contrib apps, they live at the top level, like middleware and template tags. The corresponding location for Django context processors would be django.context_processors, next to django.templatetags. However, since they're specific to the Django Template Language, django.template.context_processors seems more natural.
The CSRF processor is hardcoded in RequestContext in order to remove one configuration step and thus minimize the likelihood that users simply disable the CSRF protection.
While it isn't part of the template engine itself, the django.shortcuts module provides the render function, which is the most common entry point for rendering a template, and its sibling render_to_response.
These functions invoke render_to_string to render the template and wrap the result in a HttpResponse.
render creates a RequestContext for rendering while render_to_response uses a plain Context.
SimpleTemplateResponse and TemplateResponse are bridges between HttpResponse and the template engine. While they're defined in django.template.response, they cannot be considered as features of the template engine.
TemplateResponse creates a RequestContext for rendering while SimpleTemplateResponse uses a plain Context.
Here's a summary of the template-related APIs mentioned in the reference documentation. It encompasses all APIs that interact with other components. APIs for defining custom template tags and filters aren't included because they're internal to the Django Template Language, thus irrelevant here. All Python paths are relative to django.template.
If resolving a callable variable triggers an exception and that exception has a silent_variable_failure attribute that evaluates to True, Django will swallow the exception and render TEMPLATE_STRING_IF_INVALID.
The following private APIs might have to be made public to allow for feature parity between the Django Template Language and third-party template engines.
This section reviews dependencies on django.template or django.templatetags from other components of Django and singles out reliance on private APIs.
The list of dependencies was built by searching for from django import template and from django.template in the source tree.
Various parts of Django depend on the public APIs of Template, Context, RequestContext, and loader.
Contrib apps that provide views often import SimpleTemplateResponse or TemplateResponse.
Template tags and filters libraries in core and in contrib apps instantiate a Library.
django.test.signals depends on various internals of the template engine to reset their state when the corresponding settings change.
django.test.utils defines two context managers and decorators, override_template_loaders and override_with_test_loader, that are used by the template tests and a few others.
django.utils.translation.templatize invokes the lexer of the template engine to extract tokens and generate a pseudo-Python file that xgettext can parse.
django.views.debug relies on some internals of the template loading infrastructure.
The admindocs contrib app depends on internals of the Django Template Language to introspect template tags and filters libraries.
test_client_regress.tests.TemplateExceptionTests resets internals of the template loading infrastructure.
django.views.debug imports directly the force_escape and pprint template filters.
django.contrib.admin.helpers imports directly the capfirst and linebreaksbr template filters.
django.contrib.humanize.templatetags.humanize imports directly the date, floatformat, timesince, and timeuntil template filters.
Currently the makemessages management command is implement as follows.
It walks the filesystem under the current working directory (.).
It builds a list of files to process and corresponding locale paths.
The output of xgettext is appended to a .pot file in the target locale directory with minor adjustments.
Message catalogs ie. .po files for each language are updated according to the .pot file with msgmerge.
This section shows basic usage of common Python template engines in a web application.
All examples except Django follow the configure / load / render lifecycle.
Template engine adapters for Django would wrap these APIs.
Examples render a template called NAME = 'hello.html' found in one of TEMPLATE_DIRS with a context defined as CONTEXT = {'name': 'world'}.
from chameleon import PageTemplateLoader loader = PageTemplateLoader(TEMPLATE_DIRS) template = loader[NAME] html = template.render(**CONTEXT)
Configuration is performed by passing keyword arguments to PageTemplateLoader, which passes them to render.
from django.template import loader template = loader.get_template(NAME) html = template.render(CONTEXT)
or:
from django.template.loader import render_to_string html = render_to_string(NAME, CONTEXT)
or:
from django.template.loader import render_to_string # assuming the code is handling a HttpRequest html = render_to_string(NAME, CONTEXT, RequestContext(request))
Configuration is performed through global settings. (This is bad.)
from genshi.template import TemplateLoader loader = TemplateLoader(TEMPLATE_DIRS) template = loader.load(NAME) html = template.generate(**CONTEXT).render('html')
The author couldn't determine how configuration is performed. Genshi is more complex than other engines analyzed here.
from jinja2 import Environment, FileSystemLoader env = Environment(loader=FileSystemLoader(TEMPLATE_DIRS)) template = env.get_template(NAME) html = template.render(**CONTEXT)
Jinja2 has a concept of environment that contains global configuration. Template loading is exposed as a method of the environment.
Loaders are configured in the environment. Jinja2 provides roughly the same loaders as Django.
from mako.lookup import TemplateLookup lookup = TemplateLookup(TEMPLATE_DIRS) template = lookup.get_template(NAME) html = template.render(**CONTEXT)
Configuration is performed by passing keyword arguments to TemplateLookup, which passes them to render.
Template strings provide simplified string interpolation. They only implement rendering, with a variant that raises exceptions for missing substitutions and another variant that ignores them.
from string import Template html = Template("Hello $name").safe_substitute(**CONTEXT)
There are three maintained and mature Django - Jinja2 adapters: in chronological order, Coffin, Jingo, and Django-Jinja.
Coffin provides replacements for several Django APIs related to templates such as render. Views must use Coffin APIs explicitly.
This approach predates 44b9076b which recommends integrating third-party template engines with custom template loaders.
Coffin focuses on minimizing differences between Django and Jinja2 template by making many Django filters and tags usable from Jinja2 templates.
Jingo provides a template loader for Jinja2 templates that must be placed before Django's template loaders in TEMPLATE_LOADERS.
It provides APIs for registering globals and filters, but not tests. It recommends doing the registration in a conventional helpers submodule in installed applications.
It registers a few globals and filters, including replacements for two of Django's most useful template tags: csrf and url. However it doesn't deal with static.
It's capable of monkey-patching support for __html__ but that isn't needed any more since af64429b.
Django-Jinja replaces Django's template loaders with alternatives that handle both Jinja2 and the DTL.
It advertises wide compatibility with Django template filters and tags. The documentation doesn't talk about limitations, if any.
It integrates with Django's i18n framework, especially the makemessages management command.
It connects Jinja2's bytecode cache to Django's caching framework.
It provides APIs for registering globals and filters.
It includes url and static globals to replace Django's tags.
It supports a few popular third-party applications explicitly.
Since the Django Template Language shares some syntax with Jinja2, it's possible to write a trivial example that will work with both engines.
However, as shown above, the DTL provide several features that don't have a straightforward equivalent in Jinja2.
Porting a non-trivial application from the DTL to Jinja2 requires a significant amount of work and cannot be automated.
If you aren't convinced, try porting the django.contrib.admin templates — barely 1200 lines of template code — and see for yourself.
In order to minimize disruption for developers, this project doesn't change the default engine. However it paves the way for doing so in a later release.
No, there is no plan to deprecate it at this time.
As shown above, most Python template engines support the following pattern:
loader = TemplateLoader(**CONFIG) template = loader.load(NAME) html = template.render(**CONTEXT)
This basic API serves as a common denominator for all engines. Then each engine may expose additional features through TemplateLoader options.
First, there's a debate about the usefulness of shipping user-facing templates in pluggable apps. Templates must be customized to fit the website's design, usually by inheriting a base template. That's why many pluggable apps don't ship templates and document which templates the developer must create instead. In that case, the developer can use their favorite template engine.
If a pluggable app ships standalone templates, then which template engine they're written for doesn't matter. The author must document which template engine it uses and the developer must ensure their project meets this requirement.
Pluggable apps that provide DTL filters or tags are strongly encouraged to provide equivalent Python functions in their public APIs for interoperability with all template engines. The DTL filters or tags should be thin wrappers around the plain Python functions.
This project doesn't aim at creating Django-flavored versions of various Python template engines. It aims at building a foundation upon which every developer can create the template engine they need if it doesn't exist yet.
In other words this idea may be implemented but it belongs to a third-party module.
Likewise, these are specific features of the DTL. Other engines should provide their own APIs for loading templates and for adding common context to all templates.
Nice try ;-) This is out of scope for this project.
Thanks Collin Anderson, Loic Bistuer, Tim Graham, Jannis Leidel, Carl Meyer, Michael Manfre, Baptiste Mispelon, Daniele Procida, Josh Smeaton, and Marc Tamlyn for commenting drafts of this document. Many good ideas are theirs.
This document has been placed in the public domain per the Creative Commons CC0 1.0 Universal license.