A Python template engine for the Liquid markup language.
Project description
A Python implementation of Liquid. A non evaling templating language suitable for end users.
Installing
Install and update using pip:
$ python -m pip install -U python-liquid
Liquid requires Python>=3.7 or PyPy3.7.
Quick Start
Please see Shopify’s documentation for template syntax and a reference of available tags and filters.
An application typically creates a single Environment, with a template Loader, then loads and renders templates from that environment. This example assumes a folder called templates exists in the current working directory, and that the template file index.html exists within it.
from liquid import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("templates/"))
template = env.get_template("index.html")
print(template.render(some="variable", other="thing"))
Keyword arguments passed to the render method of a Template are made available for templates to use in template statements and expressions.
A Loader is only required if you plan to use the built-in include or render tags. You could instead create a Template directly from a string.
from liquid import Environment
env = Environment()
template = env.from_string("""
<html>
{% for i in (1..3) %}
hello {{ some }} {{ i }}
{% endfor %}
</html>
""")
print(template.render(some="variable", other="thing"))
The Environment constructor and get_template method of an environment also accept globals, a dictionary of variables made available to all templates rendered from the environment or for each call to render, respectively.
from liquid import Environment, FileSystemLoader
env = Environment(
loader=FileSystemLoader("templates/"),
globals={"site_name": "Google"},
)
template = env.get_template(
"index.html",
globals={"title": "Home Page"},
)
print(template.render(some="variable", other="thing"))
Templates are parsed and rendered in strict mode by default, where syntax and render-time type errors raise an exception as soon as possible. You can change the error tolerance mode with the tolerance argument to the Environment constructor.
Available modes are Mode.STRICT, Mode.WARN and Mode.LAX.
from liquid import Environment, FileSystemLoader, Mode
env = Environment(
loader=FileSystemLoader("templates/"),
tolerance=Mode.LAX,
)
Compatibility
We strive to be 100% compatible with the reference implementation of Liquid, written in Ruby. That is, given an equivalent render context, a template rendered with Python Liquid should produce the same output as when rendered with Ruby Liquid.
Known Issues
Please help by raising an issue if you notice an incompatibility.
Error handling. Python Liquid might not handle syntax or type errors in the same way as the reference implementation. We might fail earlier or later, and will almost certainly produce a different error message.
The built-in date filter uses dateutils for parsing strings to datetimes, and strftime for formatting. There are likely to be some inconsistencies between this and the reference implementation’s equivalent parsing and formatting of dates and times.
Benchmark
You can run the benchmark using make benchmark (or python -O performance.py if you don’t have make) from the root of the source tree. On my ropey desktop computer with a Ryzen 5 1500X, we get the following results.
Best of 5 rounds with 100 iterations per round and 60 ops per iteration (6000 ops per round).
lex template (not expressions): 1.3s (4727.35 ops/s, 78.79 i/s)
lex and parse: 6.4s (942.15 ops/s, 15.70 i/s)
render: 1.7s (3443.62 ops/s, 57.39 i/s)
lex, parse and render: 8.2s (733.30 ops/s, 12.22 i/s)
And PyPy3.7 gives us a decent increase in performance.
Best of 5 rounds with 100 iterations per round and 60 ops per iteration (6000 ops per round).
lex template (not expressions): 0.58s (10421.14 ops/s, 173.69 i/s)
lex and parse: 2.9s (2036.33 ops/s, 33.94 i/s)
render: 1.1s (5644.80 ops/s, 94.08 i/s)
lex, parse and render: 4.2s (1439.43 ops/s, 23.99 i/s)
On the same machine, running rake benchmark:run from the root of the reference implementation source tree gives us these results.
/usr/bin/ruby ./performance/benchmark.rb lax
Running benchmark for 10 seconds (with 5 seconds warmup).
Warming up --------------------------------------
parse: 3.000 i/100ms
render: 8.000 i/100ms
parse & render: 2.000 i/100ms
Calculating -------------------------------------
parse: 39.072 (± 0.0%) i/s - 393.000 in 10.058789s
render: 86.995 (± 1.1%) i/s - 872.000 in 10.024951s
parse & render: 26.139 (± 0.0%) i/s - 262.000 in 10.023365s
I’ve tried to match the benchmark workload to that of the reference implementation, so that we might compare results directly. The workload is meant to be representative of Shopify’s use case, although I wouldn’t be surprised if their usage has changed subtly since the benchmark fixture was designed.
Custom Filters
Add a custom template filter to an Environment by calling its add_filter method. A filter can be any callable that accepts at least one argument (the result of the left hand side of a filtered expression), and returns a string or object with a __str__ method.
Here’s a simple example of adding str.endswith as a filter function.
from liquid import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("templates/"))
env.add_filter("endswith", str.endswith)
And use it like this.
{% assign foo = "foobar" | endswith: "bar" %}
{% if foo %}
<!-- do something -->
{% endif %}
If you want to add more complex filters, probably including some type checking and/or casting, or the filter needs access to the active context or environment, you’ll want to inherit from Filter and implement its __call__ method.
from liquid.filter import Filter
from liquid.filter import string_required
class LinkToTag(Filter):
name = "link_to_tag"
with_context = True
@string_required
def __call__(self, label, tag, *, context):
handle = context.resolve("handle", default="")
return (
f'<a title="Show tag {tag}" href="/collections/{handle}/{tag}">{label}</a>'
)
And register it wherever you create your environment.
from liquid import Environment, FileSystemLoader
from myfilters import LinkToTag
env = Environment(loader=FileSystemLoader("templates/"))
env.add_filter(LinkToTag.name, LinkToTag(env))
In a template, you could then use the LinkToTag filter like this.
{% if tags %}
<dl class="navbar">
<dt>Tags</dt>
{% for tag in collection.tags %}
<dd>{{ tag | link_to_tag: tag }}</dd>
{% endfor %}
</dl>
{% endif %}
Note that the Filter constructor takes a single argument, a reference to the environment, which is available to Filter methods as self.env. The class variable name is used by the string_required decorator (and all other helpers/decorators found in liquid.filter) to give informative error messages.
All built-in filters are implemented in this way, so have a look in liquid/builtin/filters/ for many more examples.
Custom Loaders
Write a custom loader class by inheriting from liquid.loaders.BaseLoader and implementing its get_source method. Here we implement DictLoader, a loader that uses a dictionary of strings instead of the file system for loading templates.
from liquid.loaders import BaseLoader
from liquid.loaders import TemplateSource
from liquid.exceptions import TemplateNotFound
class DictLoader(BaseLoader):
def __init__(self, templates: Mapping[str, str]):
self.templates = templates
def get_source(self, _: Env, template_name: str) -> TemplateSource:
try:
source = self.templates[template_name]
except KeyError as err:
raise TemplateNotFound(template_name) from err
return TemplateSource(source, template_name, None)
TemplateSource is a named tuple containing the template source as a string, its name and an optional uptodate callable. If uptodate is not None it should be a callable that returns False if the template needs to be loaded again, or True otherwise.
You could then use DictLoader like this.
from liquid import Environment
from liquid.loaders import DictLoader
snippets = {
"greeting": "Hello {{ user.name }}",
"row": """
<div class="row"'
<div class="col">
{{ row_content }}
</div>
</div>
""",
}
env = Environment(loader=DictLoader(snippets))
template = env.from_string("""
<html>
{% include 'greeting' %}
{% for i in (1..3) %}
{% include 'row' with i as row_content %}
{% endfor %}
</html>
""")
print(template.render(user={"name": "Brian"}))
Contributing
Install development dependencies with Pipenv
Python Liquid fully embraces type hints and static type checking. I like to use the Pylance extension for Visual Studio Code, which includes Pyright for static type checking.
Format code using black.
Write tests using unittest.TestCase.
Run tests with make test or python -m unittest.
Check test coverage with make coverage and open htmlcov/index.html in your browser.
Check your changes have not adversely affected performance with make benchmark.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file python-liquid-0.6.3.tar.gz
.
File metadata
- Download URL: python-liquid-0.6.3.tar.gz
- Upload date:
- Size: 123.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.3.0 pkginfo/1.7.0 requests/2.25.1 setuptools/50.3.2 requests-toolbelt/0.9.1 tqdm/4.59.0 CPython/3.8.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 83d43b7301148b7df309d62dd38d6e21455c7b8f3b1d29482a3bc004aa963d0d |
|
MD5 | 834c73395ea5dfa5869d8e1fb3b93dde |
|
BLAKE2b-256 | 011223857971b1ac11996af304e80522e2e02e53901a401118cedd0dc39027d3 |
File details
Details for the file python_liquid-0.6.3-py3-none-any.whl
.
File metadata
- Download URL: python_liquid-0.6.3-py3-none-any.whl
- Upload date:
- Size: 66.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.3.0 pkginfo/1.7.0 requests/2.25.1 setuptools/50.3.2 requests-toolbelt/0.9.1 tqdm/4.59.0 CPython/3.8.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | ca36325302218f8c1656d9c4c4d9de09084384c868d02208519001c8155592f9 |
|
MD5 | e2d04444bd12e6c00d028ef003d74694 |
|
BLAKE2b-256 | 69a9d188cb25daaa67efa907632ced38d9a2087088f2f353c460c3608f8d8ffb |