Loading templates
A template loader is a class inheriting from BaseLoader
. It is responsible for finding template source text given a name or identifier, and will be called upon whenever you or a tag call Environment.get_template()
or await Environment.get_template_async()
.
To use one of the template loaders described here, pass an instance of your chosen loader as the loader
argument when constructing a Liquid Environment
.
Built-in loaders
Dictionary loader
DictLoader
is a template loader that stores template source text in memory using a dictionary. If you're experimenting with Liquid or if all your templates are known at application startup time and they all fit in RAM, then DictLoader
is a good choice.
Simply pass a dictionary mapping template names to template source text to the DictLoader
constructor.
from liquid2 import DictLoader
from liquid2 import Environment
env = Environment(
loader=DictLoader(
{
"index": "{% render 'header' %}\nbody\n{% render 'footer' %}",
"header": "some header",
"footer": "a footer",
}
)
)
template = env.get_template("index")
print(template.render())
Caching dictionary loader
CachingDictLoader
is a dictionary loader that maintains an in-memory LRU cache of parsed templates, so as to avoid parsing the same source text multiple times unnecessarily.
As well as a dictionary mapping template names to template source text, the CachingDictLoader
constructor takes an optional capacity
argument to control the maximum size of the cache. The default capacity is 300 templates.
from liquid2 import CachingDictLoader
from liquid2 import Environment
env = Environment(
loader=CachingDictLoader(
{
"index": "{% render 'header' %}\nbody\n{% render 'footer' %}",
"header": "some header",
"footer": "a footer",
}
)
)
template = env.get_template("index")
assert env.get_template("index") is template
File system loader
FileSystemLoader
is a template loader that reads source text from files on a file system. Its first argument, search_path
, is a path to a folder containing Liquid templates, either as a string or pathlib.Path
. search_path
can also be a list of paths to search in order.
In this example, calls to Environment.get_template()
will look for templates in a folder called templates
relative to the current directory.
from liquid2 import Environment
from liquid2 import FileSystemLoader
env = Environment(loader=FileSystemLoader("templates/"))
If a file called index.html
exists in ./templates
, we could render it with {% render 'index.html' %}
. To avoid having to include .html
in every render
tag, we can give FileSystemLoader
a default file extension. It should include a leading .
.
from liquid2 import Environment
from liquid2 import FileSystemLoader
env = Environment(loader=FileSystemLoader("templates/", ext=".html"))
If your templates are organized in sub folders of ./templates
, you can include the relative path to a template in a render
tag, {% render 'snippets/footer' %}
, but you won't be allowed to escape out of ./templates
. This would raise a TemplateNotFoundError
.
Caching file system loader
CachingFileSystemLoader
is a file system loader that maintains an in-memory LRU cache of parsed templates, so as to avoid reading and parsing the same source text multiple times unnecessarily.
As well as search_path
and ext
arguments covered in the file system loader section above, CachingFileSystemLoader
takes optional auto_reload
and capacity
arguments.
capacity
is the maximum number of templates that can fit in the cache and defaults to 300
templates.
auto_reload
is a flag to indicate if the template loader should check to see if each cached template has been modified since it was last loaded. If True
and template source text has been modified on-disk, that source text will automatically be read and parsed again. auto_reload
defaults to True
.
from liquid2 import Environment
from liquid2 import CachingFileSystemLoader
loader = CachingFileSystemLoader(
"/var/www/templates/",
ext=".liquid",
auto_reload=True,
capacity=1000,
)
env = Environment(loader=loader)
Package loader
PackageLoader
is a template loader that reads template source text from Python packages installed in your Python environment. You should pass the name of the package and, optionally, one or more paths to directories containing template source text within the package. The default package_path
is templates
.
Just like FileSystemLoader
, PackageLoader
accepts a default file extension, ext
. This time it defaults to .liquid
.
from liquid2 import Environment
from liquid2 import PackageLoader
loader = PackageLoader(
"awesome_templates",
package_path="path/to/templates",
ext=".liquid",
)
env = Environment(loader=loader)
Choice loader
ChoiceLoader
and CachingChoiceLoader
are template loaders that delegate to a list of other template loaders. Each one is tried in turn until a template is found.
When using CachingChoiceLoader
, you should probably avoid delegating to other caching loaders.
from liquid2 import CachingFileSystemLoader
from liquid2 import ChoiceLoader
from liquid2 import DictLoader
from liquid2 import Environment
base_loader = DictLoader({"foo": "some template source text"})
overlay_loader = CachingFileSystemLoader("templates/")
loader = ChoiceLoader([overlay_loader, base_loader])
env = Environment(loader=loader)
Custom loaders
If you want to load templates from a database or over a network, you'll need to write your own template loader. Simply inherit from BaseLoader
and implement get_source()
and, possibly, get_source_async()
.
get_source()
should return an instance of TemplateSource
, which is a named tuple containing source text, the template's name, an optional uptodate
callable and any extra data you want bound to the template.
Take a look at source code for the built-in loaders for examples.
Load context
get_source()
takes an optional context
argument. When Environment.get_template()
is called from a tag, like render
, the active render context will be passed along to get_source()
. Loaders can then choose to use render context data to dynamically refine template names and loader search paths.
Arbitrary keyword arguments can also be passed to Environment.get_template()
. These too are passed along to get_source()
, and can also be used by custom template loaders to refine the template source search space.
For example, all built-in tags that call Environment.get_template()
pass a keyword argument called tag
set to the name of the calling tag. We can use the tag name to mimic Shopify's snippets
convention, where {% include %}
and {% render %}
automatically load templates from a subfolder call snippets
.
from pathlib import Path
from liquid2 import CachingFileSystemLoader
from liquid2 import Environment
from liquid2 import RenderContext
from liquid2 import TemplateSource
class SnippetsFileSystemLoader(CachingFileSystemLoader):
def get_source(
self,
env: Environment,
template_name: str,
*,
context: RenderContext | None = None,
**kwargs: object,
) -> TemplateSource:
if kwargs.get("tag") in ("include", "render"):
snippet = Path("snippets").joinpath(template_name)
return super().get_source(
env, template_name=str(snippet), context=context, **kwargs
)
return super().get_source(
env, template_name=template_name, context=context, **kwargs
)
Matter
Sometimes template source text comes with associated data. This could be meta data read from a database or front matter read from the top of the file containing template source text. The TemplateSource
object returned from get_source()
facilitates these cases with matter
, a dictionary mapping strings to arbitrary objects that will be merged with environment and template globals and bound to the resulting Template
instance.
Here's an example of a template loader that reads front matter in YAML format.
import re
import yaml
from liquid2 import CachingFileSystemLoader
from liquid2 import Environment
from liquid2 import RenderContext
from liquid2 import TemplateSource
RE_FRONT_MATTER = re.compile(r"\s*---\s*(.*?)\s*---\s*", re.MULTILINE | re.DOTALL)
class FrontMatterLoader(CachingFileSystemLoader):
def get_source(
self,
env: Environment,
template_name: str,
*,
context: RenderContext | None = None,
**kwargs: object,
) -> TemplateSource:
source, filename, uptodate, matter = super().get_source(env, template_name)
match = RE_FRONT_MATTER.search(source)
if match:
# TODO: add some yaml error handling here.
matter = yaml.load(match.group(1), Loader=yaml.SafeLoader)
source = source[match.end() :]
return TemplateSource(
source,
filename,
uptodate,
matter,
)
Caching mixin
Use CachingLoaderMixin
to add in-memory LRU caching to your custom template loaders. For example, here's the definition of CachingDictLoader
.
from liquid2 import CachingLoaderMixin
from liquid2 import DictLoader
class CachingDictLoader(CachingLoaderMixin, DictLoader):
"""A `DictLoader` that caches parsed templates in memory."""
def __init__(
self,
templates: dict[str, str],
*,
auto_reload: bool = True,
namespace_key: str = "",
capacity: int = 300,
):
super().__init__(
auto_reload=auto_reload,
namespace_key=namespace_key,
capacity=capacity,
)
DictLoader.__init__(self, templates)