Variables, types and drops
Liquid primitive types map to Python types according to the following table. You can, for example, compare a Liquid string to a Python string directly with {% if var == "thing" %}
, where var
is a global variable containing a Python string.
Note that Liquid has weak typing. Anywhere a particular type is expected, Liquid will implicitly try to convert a value to that type if needed.
Sequences and mappings
Anywhere an array-like value is expected, like the left-hand side of the join
filter, Liquid will accept any Python Sequence, not just a list.
In the case of a Mapping, like a dict, a {% for %}
loop will iterator over mapping items, whereas a sequence filter will add the mapping to a single element sequence and iterate over that.
from collections.abc import Sequence
from liquid2 import render
class MySequence(Sequence[int]):
def __init__(self, items: list[int]):
self.items = items
def __getitem__(self, key: int) -> int:
return self.items[key] * 2
def __len__(self) -> int:
return len(self.items)
data = {
"s": MySequence([1, 2, 3]),
"l": ["a", "b", "c"],
"t": (True, False),
"d": {"x": 4, "y": 5, "z": 6}
}
source = """\
{% assign sequences = s, l, t, d -%}
{% for sequence in sequences -%}
{% for item in sequence ~%}
- {{ item -}}
{% endfor %}
{% endfor %}
"""
print(render(source, **data))
Paths to variables
When referenced in a template, a variable is best viewed as a path to a value, where each path has one or more segments. Segments can be property names separated by dots (foo.bar
), array indexes using bracket notation (store.products[1]
) or bracketed property names for situations where the property name is held in a variable or contains reserved characters (product.variant[var]
or products["something with spaces"]
)
Python Liquid uses __getitem__
internally for resolving property names and accessing items in a sequence. So, if your data is some combination of dictionaries and lists, for example, templates can reference objects as follows.
{
"products": [
{
"title": "Some Shoes",
"available": 5,
"colors": ["blue", "red"]
},
{
"title": "A Hat",
"available": 2,
"colors": ["grey", "brown"]
}
]
}
{{ products[0].title }}
{{ products[-2]['available'] }}
{{ products.last.title }}
{{ products.first.colors | join: ', ' }}
Attempting to access properties from a Python class or class instance will not work.
from liquid import Template
class Product:
def __init__(self, title, colors):
self.title = title
self.colors = colors
products = [
Product(title="Some Shoes", colors=["blue", "red"]),
Product(title="A Hat", colors=["grey", "brown"]),
]
Template("{{ products.first.title }}!").render(products=products)
Drops
A drop (as in "drop of liquid") is an instance of a Python class that implements the Sequence or Mapping interface, or other magic methods.
We use the Mapping interface to force ourselves to be explicit about which properties are exposed to template authors.
from collections import abc
from typing import Any
from liquid2 import Environment
from liquid2 import StrictUndefined
from liquid2 import render
class User(abc.Mapping[str, Any]):
def __init__(
self,
first_name: str,
last_name: str,
perms: list[str],
):
self.first_name = first_name
self.last_name = last_name
self.perms = perms or []
self._keys = [
"first_name",
"last_name",
"is_admin",
"name",
]
def __getitem__(self, k):
if k in self._keys:
return getattr(self, k)
raise KeyError(k)
def __iter__(self):
return iter(self._keys)
def __len__(self):
return len(self._keys)
def __str__(self):
return f"User(first_name='{self.first_name}', last_name='{self.last_name}')"
@property
def is_admin(self):
return "admin" in self.perms
@property
def name(self):
return f"{self.first_name} {self.last_name}"
user = User("John", "Smith", ["admin"])
print(render("{{ user.first_name }}", user=user)) # John
print(render("{{ user.name }}", user=user)) # John Smith
print(render("{{ user.is_admin }}", user=user)) # true
strict_env = Environment(undefined=StrictUndefined)
print(strict_env.from_string("{{ user.perms[0] }}").render(user=user))
# liquid2.exceptions.UndefinedError: user.perms is undefined
# -> '{{ user.perms[0] }}' 1:3
# |
# 1 | {{ user.perms[0] }}
# | ^^^^^^^^^^^^^ user.perms is undefined
Drop wrapper
For convenience, you could implement a drop wrapper for data access objects, while still being explicit about which properties to expose.
class Drop(abc.Mapping):
def __init__(obj, keys):
self.obj = obj
self.keys = keys
def __getitem__(self, k):
# Delegate attribute access to self.obj only if `k` is in `self.keys`.
if k in self.keys:
return getattr(obj, k)
raise KeyError(k)
def __iter__(self):
return iter(self.keys)
def __len__(self):
return len(self.keys)
__liquid__
If a drop implements the special __liquid__()
method, Liquid will use the result of calling __liquid__()
when resolving a variable path or segment. This is useful for situations where you need your Python object to act as an array index, or to be compared to a primitive data type, for example.
from liquid2 import parse
class IntDrop:
def __init__(self, val: int):
self.val = val
def __int__(self) -> int:
return self.val
def __str__(self) -> str:
return "one"
def __liquid__(self) -> int:
return self.val
template = parse(
"{% if my_drop < 10 %}"
"{{ my_drop }} "
"{% endif %}"
"{{ some_array[my_drop] }}"
)
context_data = {
"my_drop": IntDrop(1),
"some_array": ["a", "b", "c"],
}
print(template.render(**context_data)) # one b
__html__
When HTML auto-escaping is enabled, an object can be output as an HTML-safe string by implementing the special __html__()
method.
from liquid2 import Environment
class ListDrop:
def __init__(self, somelist):
self.items = somelist
def __str__(self):
return f"ListDrop({self.items})"
def __html__(self):
lis = "\n".join(f" <li>{item}</li>" for item in self.items)
return f"<ul>\n{lis}\n</ul>"
env = Environment(auto_escape=True)
template = env.from_string(r"{{ products }}")
print(template.render(products=ListDrop(["Shoe", "Hat", "Ball"])))
__getitem_async__
If an instance of a drop that implements __getitem_async__()
appears in a render_async()
context, __getitem_async__()
will be awaited instead of calling __getitem__()
.
class AsyncCollection(abc.Mapping):
def __init__(self, val):
self.keys = ["products"]
self.cached_products = []
def __len__(self):
return 1
def __iter__(self):
return iter(self["products"])
async def __aiter__(self):
# Note that Liquid's built-in `for` loop does not yet support async iteration.
return iter(self.__getitem_async__("products"))
def __getitem__(self, k):
if not self.cached_products:
# Blocking IO here
self.cached_products = get_stuff_from_database()
return self.cache_products
async def __getitem_async__(self, k):
if not self.cached_products:
# Do async IO here.
self.cached_products = await get_stuff_from_database_async()
return self.cache_products
Other magic methods
Other Python magic methods will work with Liquid filters and special properties too.
from liquid2 import Environment
env = Environment()
class Foo:
def __int__(self):
return 7
def __str__(self):
return "Bar"
def __len__(self):
return 5
template = env.from_string(
"""\
{{ foo }}
{{ foo | plus: 2 }}
{{ foo.size }}
"""
)
print(template.render(foo=Foo()))
Undefined variables
At render time, if a variable can not be resolved, and instance of Undefined
is used instead. We can customize template rendering behavior by implementing some of Python's "magic" methods on a subclass of Undefined
.
Default undefined
All operations on the default Undefined
type are silently ignored and, when rendered, it produces an empty string. For example, you can access properties and iterate an undefined variable without error.
Strict undefined
When StrictUndefined
is passed as the undefined
argument to an Environment
, any operation on an undefined variable will raise an UndefinedError
.
from liquid2 import Environment, StrictUndefined
env = Environment(undefined=StrictUndefined)
template = env.from_string("Hello {{ nosuchthing }}")
template.render()
# liquid2.exceptions.UndefinedError: 'nosuchthing' is undefined
# -> 'Hello {{ nosuchthing }}' 1:9
# |
# 1 | Hello {{ nosuchthing }}
# | ^^^^^^^^^^^ 'nosuchthing' is undefined
Falsy strict undefined
FalsyStrictUndefined
is the same as StrictUndefined
, but can be tested for truthiness and equality without raising an exception.